| 'use strict'; |
| const { lstatSync, readdirSync } = require('fs'); |
| const { lstat, readdir } = require('fs/promises'); |
| const { join, resolve } = require('path'); |
| |
| const { |
| kEmptyObject, |
| } = require('internal/util'); |
| const { |
| validateFunction, |
| validateObject, |
| validateString, |
| validateStringArray, |
| } = require('internal/validators'); |
| |
| const { |
| ArrayFrom, |
| ArrayPrototypeAt, |
| ArrayPrototypeMap, |
| ArrayPrototypeFlatMap, |
| ArrayPrototypePop, |
| ArrayPrototypePush, |
| ArrayPrototypeSome, |
| PromisePrototypeThen, |
| SafeMap, |
| SafeSet, |
| StringPrototypeEndsWith, |
| } = primordials; |
| |
| let minimatch; |
| function lazyMinimatch() { |
| minimatch ??= require('internal/deps/minimatch/index'); |
| return minimatch; |
| } |
| |
| const isWindows = process.platform === 'win32'; |
| const isOSX = process.platform === 'darwin'; |
| |
| class Cache { |
| #cache = new SafeMap(); |
| #statsCache = new SafeMap(); |
| #readdirCache = new SafeMap(); |
| |
| stat(path) { |
| const cached = this.#statsCache.get(path); |
| if (cached) { |
| return cached; |
| } |
| const promise = PromisePrototypeThen(lstat(path), null, () => null); |
| this.#statsCache.set(path, promise); |
| return promise; |
| } |
| statSync(path) { |
| const cached = this.#statsCache.get(path); |
| if (cached) { |
| return cached; |
| } |
| let val; |
| try { |
| val = lstatSync(path); |
| } catch { |
| val = null; |
| } |
| this.#statsCache.set(path, val); |
| return val; |
| } |
| addToStatCache(path, val) { |
| this.#statsCache.set(path, val); |
| } |
| async readdir(path) { |
| const cached = this.#readdirCache.get(path); |
| if (cached) { |
| return cached; |
| } |
| const promise = PromisePrototypeThen(readdir(path, { __proto__: null, withFileTypes: true }), null, () => null); |
| this.#readdirCache.set(path, promise); |
| return promise; |
| } |
| readdirSync(path) { |
| const cached = this.#readdirCache.get(path); |
| if (cached) { |
| return cached; |
| } |
| let val; |
| try { |
| val = readdirSync(path, { __proto__: null, withFileTypes: true }); |
| } catch { |
| val = []; |
| } |
| this.#readdirCache.set(path, val); |
| return val; |
| } |
| add(path, pattern) { |
| let cache = this.#cache.get(path); |
| if (!cache) { |
| cache = new SafeSet(); |
| this.#cache.set(path, cache); |
| } |
| const originalSize = cache.size; |
| pattern.indexes.forEach((index) => cache.add(pattern.cacheKey(index))); |
| return cache.size !== originalSize + pattern.indexes.size; |
| } |
| seen(path, pattern, index) { |
| return this.#cache.get(path)?.has(pattern.cacheKey(index)); |
| } |
| } |
| |
| class Pattern { |
| #pattern; |
| #globStrings; |
| indexes; |
| symlinks; |
| last; |
| |
| constructor(pattern, globStrings, indexes, symlinks) { |
| this.#pattern = pattern; |
| this.#globStrings = globStrings; |
| this.indexes = indexes; |
| this.symlinks = symlinks; |
| this.last = pattern.length - 1; |
| } |
| |
| isLast(isDirectory) { |
| return this.indexes.has(this.last) || |
| (this.at(-1) === '' && isDirectory && |
| this.indexes.has(this.last - 1) && this.at(-2) === lazyMinimatch().GLOBSTAR); |
| } |
| isFirst() { |
| return this.indexes.has(0); |
| } |
| get hasSeenSymlinks() { |
| return ArrayPrototypeSome(ArrayFrom(this.indexes), (i) => !this.symlinks.has(i)); |
| } |
| at(index) { |
| return ArrayPrototypeAt(this.#pattern, index); |
| } |
| child(indexes, symlinks = new SafeSet()) { |
| return new Pattern(this.#pattern, this.#globStrings, indexes, symlinks); |
| } |
| test(index, path) { |
| if (index > this.#pattern.length) { |
| return false; |
| } |
| const pattern = this.#pattern[index]; |
| if (pattern === lazyMinimatch().GLOBSTAR) { |
| return true; |
| } |
| if (typeof pattern === 'string') { |
| return pattern === path; |
| } |
| if (typeof pattern?.test === 'function') { |
| return pattern.test(path); |
| } |
| return false; |
| } |
| |
| cacheKey(index) { |
| let key = ''; |
| for (let i = index; i < this.#globStrings.length; i++) { |
| key += this.#globStrings[i]; |
| if (i !== this.#globStrings.length - 1) { |
| key += '/'; |
| } |
| } |
| return key; |
| } |
| } |
| |
| class Glob { |
| #root; |
| #exclude; |
| #cache = new Cache(); |
| #results = new SafeSet(); |
| #queue = []; |
| #subpatterns = new SafeMap(); |
| #patterns; |
| constructor(pattern, options = kEmptyObject) { |
| validateObject(options, 'options'); |
| const { exclude, cwd } = options; |
| if (exclude != null) { |
| validateFunction(exclude, 'options.exclude'); |
| } |
| this.#root = cwd ?? '.'; |
| this.#exclude = exclude; |
| let patterns; |
| if (typeof pattern === 'object') { |
| validateStringArray(pattern, 'patterns'); |
| patterns = pattern; |
| } else { |
| validateString(pattern, 'patterns'); |
| patterns = [pattern]; |
| } |
| this.matchers = ArrayPrototypeMap(patterns, (pattern) => new (lazyMinimatch().Minimatch)(pattern, { |
| __proto__: null, |
| nocase: isWindows || isOSX, |
| windowsPathsNoEscape: true, |
| nonegate: true, |
| nocomment: true, |
| optimizationLevel: 2, |
| platform: process.platform, |
| nocaseMagicOnly: true, |
| })); |
| |
| this.#patterns = ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set, |
| (pattern, i) => new Pattern( |
| pattern, |
| matcher.globParts[i], |
| new SafeSet().add(0), |
| new SafeSet(), |
| ))); |
| } |
| |
| globSync() { |
| ArrayPrototypePush(this.#queue, { __proto__: null, path: '.', patterns: this.#patterns }); |
| while (this.#queue.length > 0) { |
| const item = ArrayPrototypePop(this.#queue); |
| for (let i = 0; i < item.patterns.length; i++) { |
| this.#addSubpatterns(item.path, item.patterns[i]); |
| } |
| this.#subpatterns |
| .forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns })); |
| this.#subpatterns.clear(); |
| } |
| return ArrayFrom(this.#results); |
| } |
| #addSubpattern(path, pattern) { |
| if (!this.#subpatterns.has(path)) { |
| this.#subpatterns.set(path, [pattern]); |
| } else { |
| ArrayPrototypePush(this.#subpatterns.get(path), pattern); |
| } |
| } |
| #addSubpatterns(path, pattern) { |
| const seen = this.#cache.add(path, pattern); |
| if (seen) { |
| return; |
| } |
| const fullpath = resolve(this.#root, path); |
| const stat = this.#cache.statSync(fullpath); |
| const last = pattern.last; |
| const isDirectory = stat?.isDirectory() || (stat?.isSymbolicLink() && pattern.hasSeenSymlinks); |
| const isLast = pattern.isLast(isDirectory); |
| const isFirst = pattern.isFirst(); |
| |
| if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) { |
| // Absolute path, go to root |
| this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1))); |
| return; |
| } |
| if (isFirst && pattern.at(0) === '') { |
| // Absolute path, go to root |
| this.#addSubpattern('/', pattern.child(new SafeSet().add(1))); |
| return; |
| } |
| if (isFirst && pattern.at(0) === '..') { |
| // Start with .., go to parent |
| this.#addSubpattern('../', pattern.child(new SafeSet().add(1))); |
| return; |
| } |
| if (isFirst && pattern.at(0) === '.') { |
| // Start with ., proceed |
| this.#addSubpattern('.', pattern.child(new SafeSet().add(1))); |
| return; |
| } |
| |
| if (isLast && typeof pattern.at(-1) === 'string') { |
| // Add result if it exists |
| const p = pattern.at(-1); |
| const stat = this.#cache.statSync(join(fullpath, p)); |
| if (stat && (p || isDirectory)) { |
| this.#results.add(join(path, p)); |
| } |
| if (pattern.indexes.size === 1 && pattern.indexes.has(last)) { |
| return; |
| } |
| } else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR && |
| (path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) { |
| // If pattern ends with **, add to results |
| // if path is ".", add it only if pattern starts with "." or pattern is exactly "**" |
| this.#results.add(path); |
| } |
| |
| if (!isDirectory) { |
| return; |
| } |
| |
| let children; |
| const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value); |
| if (typeof firstPattern === 'string') { |
| const stat = this.#cache.statSync(join(fullpath, firstPattern)); |
| if (stat) { |
| stat.name = firstPattern; |
| children = [stat]; |
| } else { |
| return; |
| } |
| } else { |
| children = this.#cache.readdirSync(fullpath); |
| } |
| |
| for (let i = 0; i < children.length; i++) { |
| const entry = children[i]; |
| const entryPath = join(path, entry.name); |
| this.#cache.addToStatCache(join(fullpath, entry.name), entry); |
| |
| const subPatterns = new SafeSet(); |
| const nSymlinks = new SafeSet(); |
| for (const index of pattern.indexes) { |
| // For each child, chek potential patterns |
| if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) { |
| return; |
| } |
| const current = pattern.at(index); |
| const nextIndex = index + 1; |
| const next = pattern.at(nextIndex); |
| const fromSymlink = pattern.symlinks.has(index); |
| |
| if (current === lazyMinimatch().GLOBSTAR) { |
| if (entry.name[0] === '.' || (this.#exclude && this.#exclude(entry.name))) { |
| continue; |
| } |
| if (!fromSymlink && entry.isDirectory()) { |
| // If directory, add ** to its potential patterns |
| subPatterns.add(index); |
| } else if (!fromSymlink && index === last) { |
| // If ** is last, add to results |
| this.#results.add(entryPath); |
| } |
| |
| // Any pattern after ** is also a potential pattern |
| // so we can already test it here |
| const nextMatches = pattern.test(nextIndex, entry.name); |
| if (nextMatches && nextIndex === last && !isLast) { |
| // If next pattern is the last one, add to results |
| this.#results.add(entryPath); |
| } else if (nextMatches && entry.isDirectory()) { |
| // Pattern mached, meaning two patterns forward |
| // are also potential patterns |
| // e.g **/b/c when entry is a/b - add c to potential patterns |
| subPatterns.add(index + 2); |
| } |
| if ((nextMatches || pattern.at(0) === '.') && |
| (entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) { |
| // If pattern after ** matches, or pattern starts with "." |
| // and entry is a directory or symlink, add to potential patterns |
| subPatterns.add(nextIndex); |
| } |
| |
| if (entry.isSymbolicLink()) { |
| nSymlinks.add(index); |
| } |
| |
| if (next === '..' && entry.isDirectory()) { |
| // In case pattern is "**/..", |
| // both parent and current directory should be added to the queue |
| // if this is the last pattern, add to results instead |
| const parent = join(path, '..'); |
| if (nextIndex < last) { |
| if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) { |
| this.#subpatterns.set(path, [pattern.child(new SafeSet().add(nextIndex + 1))]); |
| } |
| if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) { |
| this.#subpatterns.set(parent, [pattern.child(new SafeSet().add(nextIndex + 1))]); |
| } |
| } else { |
| if (!this.#cache.seen(path, pattern, nextIndex)) { |
| this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex))); |
| this.#results.add(path); |
| } |
| if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) { |
| this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex))); |
| this.#results.add(parent); |
| } |
| } |
| } |
| } |
| if (typeof current === 'string') { |
| if (pattern.test(index, entry.name) && index !== last) { |
| // If current pattern matches entry name |
| // the next pattern is a potential pattern |
| subPatterns.add(nextIndex); |
| } else if (current === '.' && pattern.test(nextIndex, entry.name)) { |
| // If current pattern is ".", proceed to test next pattern |
| if (nextIndex === last) { |
| this.#results.add(entryPath); |
| } else { |
| subPatterns.add(nextIndex + 1); |
| } |
| } |
| } |
| if (typeof current === 'object' && pattern.test(index, entry.name)) { |
| // If current pattern is a regex that matches entry name (e.g *.js) |
| // add next pattern to potential patterns, or to results if it's the last pattern |
| if (index === last) { |
| this.#results.add(entryPath); |
| } else if (entry.isDirectory()) { |
| subPatterns.add(nextIndex); |
| } |
| } |
| } |
| if (subPatterns.size > 0) { |
| // If there are potential patterns, add to queue |
| this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks)); |
| } |
| } |
| } |
| |
| |
| async* glob() { |
| ArrayPrototypePush(this.#queue, { __proto__: null, path: '.', patterns: this.#patterns }); |
| while (this.#queue.length > 0) { |
| const item = ArrayPrototypePop(this.#queue); |
| for (let i = 0; i < item.patterns.length; i++) { |
| yield* this.#iterateSubpatterns(item.path, item.patterns[i]); |
| } |
| this.#subpatterns |
| .forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns })); |
| this.#subpatterns.clear(); |
| } |
| } |
| async* #iterateSubpatterns(path, pattern) { |
| const seen = this.#cache.add(path, pattern); |
| if (seen) { |
| return; |
| } |
| const fullpath = resolve(this.#root, path); |
| const stat = await this.#cache.stat(fullpath); |
| const last = pattern.last; |
| const isDirectory = stat?.isDirectory() || (stat?.isSymbolicLink() && pattern.hasSeenSymlinks); |
| const isLast = pattern.isLast(isDirectory); |
| const isFirst = pattern.isFirst(); |
| |
| if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) { |
| // Absolute path, go to root |
| this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1))); |
| return; |
| } |
| if (isFirst && pattern.at(0) === '') { |
| // Absolute path, go to root |
| this.#addSubpattern('/', pattern.child(new SafeSet().add(1))); |
| return; |
| } |
| if (isFirst && pattern.at(0) === '..') { |
| // Start with .., go to parent |
| this.#addSubpattern('../', pattern.child(new SafeSet().add(1))); |
| return; |
| } |
| if (isFirst && pattern.at(0) === '.') { |
| // Start with ., proceed |
| this.#addSubpattern('.', pattern.child(new SafeSet().add(1))); |
| return; |
| } |
| |
| if (isLast && typeof pattern.at(-1) === 'string') { |
| // Add result if it exists |
| const p = pattern.at(-1); |
| const stat = await this.#cache.stat(join(fullpath, p)); |
| if (stat && (p || isDirectory)) { |
| const result = join(path, p); |
| if (!this.#results.has(result)) { |
| this.#results.add(result); |
| yield result; |
| } |
| } |
| if (pattern.indexes.size === 1 && pattern.indexes.has(last)) { |
| return; |
| } |
| } else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR && |
| (path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) { |
| // If pattern ends with **, add to results |
| // if path is ".", add it only if pattern starts with "." or pattern is exactly "**" |
| if (!this.#results.has(path)) { |
| this.#results.add(path); |
| yield path; |
| } |
| } |
| |
| if (!isDirectory) { |
| return; |
| } |
| |
| let children; |
| const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value); |
| if (typeof firstPattern === 'string') { |
| const stat = await this.#cache.stat(join(fullpath, firstPattern)); |
| if (stat) { |
| stat.name = firstPattern; |
| children = [stat]; |
| } else { |
| return; |
| } |
| } else { |
| children = await this.#cache.readdir(fullpath); |
| } |
| |
| for (let i = 0; i < children.length; i++) { |
| const entry = children[i]; |
| const entryPath = join(path, entry.name); |
| this.#cache.addToStatCache(join(fullpath, entry.name), entry); |
| |
| const subPatterns = new SafeSet(); |
| const nSymlinks = new SafeSet(); |
| for (const index of pattern.indexes) { |
| // For each child, chek potential patterns |
| if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) { |
| return; |
| } |
| const current = pattern.at(index); |
| const nextIndex = index + 1; |
| const next = pattern.at(nextIndex); |
| const fromSymlink = pattern.symlinks.has(index); |
| |
| if (current === lazyMinimatch().GLOBSTAR) { |
| if (entry.name[0] === '.' || (this.#exclude && this.#exclude(entry.name))) { |
| continue; |
| } |
| if (!fromSymlink && entry.isDirectory()) { |
| // If directory, add ** to its potential patterns |
| subPatterns.add(index); |
| } else if (!fromSymlink && index === last) { |
| // If ** is last, add to results |
| if (!this.#results.has(entryPath)) { |
| this.#results.add(entryPath); |
| yield entryPath; |
| } |
| } |
| |
| // Any pattern after ** is also a potential pattern |
| // so we can already test it here |
| const nextMatches = pattern.test(nextIndex, entry.name); |
| if (nextMatches && nextIndex === last && !isLast) { |
| // If next pattern is the last one, add to results |
| if (!this.#results.has(entryPath)) { |
| this.#results.add(entryPath); |
| yield entryPath; |
| } |
| } else if (nextMatches && entry.isDirectory()) { |
| // Pattern mached, meaning two patterns forward |
| // are also potential patterns |
| // e.g **/b/c when entry is a/b - add c to potential patterns |
| subPatterns.add(index + 2); |
| } |
| if ((nextMatches || pattern.at(0) === '.') && |
| (entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) { |
| // If pattern after ** matches, or pattern starts with "." |
| // and entry is a directory or symlink, add to potential patterns |
| subPatterns.add(nextIndex); |
| } |
| |
| if (entry.isSymbolicLink()) { |
| nSymlinks.add(index); |
| } |
| |
| if (next === '..' && entry.isDirectory()) { |
| // In case pattern is "**/..", |
| // both parent and current directory should be added to the queue |
| // if this is the last pattern, add to results instead |
| const parent = join(path, '..'); |
| if (nextIndex < last) { |
| if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) { |
| this.#subpatterns.set(path, [pattern.child(new SafeSet().add(nextIndex + 1))]); |
| } |
| if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) { |
| this.#subpatterns.set(parent, [pattern.child(new SafeSet().add(nextIndex + 1))]); |
| } |
| } else { |
| if (!this.#cache.seen(path, pattern, nextIndex)) { |
| this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex))); |
| if (!this.#results.has(path)) { |
| this.#results.add(path); |
| yield path; |
| } |
| } |
| if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) { |
| this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex))); |
| if (!this.#results.has(parent)) { |
| this.#results.add(parent); |
| yield parent; |
| } |
| } |
| } |
| } |
| } |
| if (typeof current === 'string') { |
| if (pattern.test(index, entry.name) && index !== last) { |
| // If current pattern matches entry name |
| // the next pattern is a potential pattern |
| subPatterns.add(nextIndex); |
| } else if (current === '.' && pattern.test(nextIndex, entry.name)) { |
| // If current pattern is ".", proceed to test next pattern |
| if (nextIndex === last) { |
| if (!this.#results.has(entryPath)) { |
| this.#results.add(entryPath); |
| yield entryPath; |
| } |
| } else { |
| subPatterns.add(nextIndex + 1); |
| } |
| } |
| } |
| if (typeof current === 'object' && pattern.test(index, entry.name)) { |
| // If current pattern is a regex that matches entry name (e.g *.js) |
| // add next pattern to potential patterns, or to results if it's the last pattern |
| if (index === last) { |
| if (!this.#results.has(entryPath)) { |
| this.#results.add(entryPath); |
| yield entryPath; |
| } |
| } else if (entry.isDirectory()) { |
| subPatterns.add(nextIndex); |
| } |
| } |
| } |
| if (subPatterns.size > 0) { |
| // If there are potential patterns, add to queue |
| this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks)); |
| } |
| } |
| } |
| } |
| |
| module.exports = { |
| __proto__: null, |
| Glob, |
| }; |