Skip to content

Commit 1948dce

Browse files
MoLownodejs-github-bot
authored andcommitted
fs: add globSync implementation
this is currently for internal use only, with a synchrnous API. PR-URL: #47653 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 71d7707 commit 1948dce

File tree

2 files changed

+693
-0
lines changed

2 files changed

+693
-0
lines changed

lib/internal/fs/glob.js

+384
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
'use strict';
2+
const { lstatSync, readdirSync } = require('fs');
3+
const { join, resolve } = require('path');
4+
5+
const {
6+
kEmptyObject,
7+
} = require('internal/util');
8+
const {
9+
validateFunction,
10+
validateObject,
11+
} = require('internal/validators');
12+
13+
const {
14+
ArrayFrom,
15+
ArrayPrototypeAt,
16+
ArrayPrototypeMap,
17+
ArrayPrototypeFlatMap,
18+
ArrayPrototypePop,
19+
ArrayPrototypePush,
20+
ArrayPrototypeSome,
21+
SafeMap,
22+
SafeSet,
23+
StringPrototypeEndsWith,
24+
} = primordials;
25+
26+
let minimatch;
27+
function lazyMinimatch() {
28+
minimatch ??= require('internal/deps/minimatch/index');
29+
return minimatch;
30+
}
31+
32+
const isWindows = process.platform === 'win32';
33+
const isOSX = process.platform === 'darwin';
34+
35+
class Cache {
36+
#cache = new SafeMap();
37+
#statsCache = new SafeMap();
38+
#readdirCache = new SafeMap();
39+
40+
statSync(path) {
41+
const cached = this.#statsCache.get(path);
42+
if (cached) {
43+
return cached;
44+
}
45+
let val;
46+
try {
47+
val = lstatSync(path);
48+
} catch {
49+
val = null;
50+
}
51+
this.#statsCache.set(path, val);
52+
return val;
53+
}
54+
addToStatCache(path, val) {
55+
this.#statsCache.set(path, val);
56+
}
57+
readdirSync(path) {
58+
const cached = this.#readdirCache.get(path);
59+
if (cached) {
60+
return cached;
61+
}
62+
let val;
63+
try {
64+
val = readdirSync(path, { __proto__: null, withFileTypes: true });
65+
} catch {
66+
val = [];
67+
}
68+
this.#readdirCache.set(path, val);
69+
return val;
70+
}
71+
add(path, pattern) {
72+
let cache = this.#cache.get(path);
73+
if (!cache) {
74+
cache = new SafeSet();
75+
this.#cache.set(path, cache);
76+
}
77+
const originalSize = cache.size;
78+
pattern.indexes.forEach((index) => cache.add(pattern.cacheKey(index)));
79+
return cache.size !== originalSize + pattern.indexes.size;
80+
}
81+
seen(path, pattern, index) {
82+
return this.#cache.get(path)?.has(pattern.cacheKey(index));
83+
}
84+
}
85+
86+
class Pattern {
87+
#pattern;
88+
#globStrings;
89+
indexes;
90+
symlinks;
91+
last;
92+
93+
constructor(pattern, globStrings, indexes, symlinks) {
94+
this.#pattern = pattern;
95+
this.#globStrings = globStrings;
96+
this.indexes = indexes;
97+
this.symlinks = symlinks;
98+
this.last = pattern.length - 1;
99+
}
100+
101+
isLast(isDirectory) {
102+
return this.indexes.has(this.last) ||
103+
(this.at(-1) === '' && isDirectory &&
104+
this.indexes.has(this.last - 1) && this.at(-2) === lazyMinimatch().GLOBSTAR);
105+
}
106+
isFirst() {
107+
return this.indexes.has(0);
108+
}
109+
get hasSeenSymlinks() {
110+
return ArrayPrototypeSome(ArrayFrom(this.indexes), (i) => !this.symlinks.has(i));
111+
}
112+
at(index) {
113+
return ArrayPrototypeAt(this.#pattern, index);
114+
}
115+
child(indexes, symlinks = new SafeSet()) {
116+
return new Pattern(this.#pattern, this.#globStrings, indexes, symlinks);
117+
}
118+
test(index, path) {
119+
if (index > this.#pattern.length) {
120+
return false;
121+
}
122+
const pattern = this.#pattern[index];
123+
if (pattern === lazyMinimatch().GLOBSTAR) {
124+
return true;
125+
}
126+
if (typeof pattern === 'string') {
127+
return pattern === path;
128+
}
129+
if (typeof pattern?.test === 'function') {
130+
return pattern.test(path);
131+
}
132+
return false;
133+
}
134+
135+
cacheKey(index) {
136+
let key = '';
137+
for (let i = index; i < this.#globStrings.length; i++) {
138+
key += this.#globStrings[i];
139+
if (i !== this.#globStrings.length - 1) {
140+
key += '/';
141+
}
142+
}
143+
return key;
144+
}
145+
}
146+
147+
class Glob {
148+
#root;
149+
#exclude;
150+
#cache = new Cache();
151+
#results = [];
152+
#queue = [];
153+
#subpatterns = new SafeMap();
154+
constructor(patterns, options = kEmptyObject) {
155+
validateObject(options, 'options');
156+
const { exclude, cwd } = options;
157+
if (exclude != null) {
158+
validateFunction(exclude, 'options.exclude');
159+
}
160+
this.#root = cwd ?? '.';
161+
this.#exclude = exclude;
162+
this.matchers = ArrayPrototypeMap(patterns, (pattern) => new (lazyMinimatch().Minimatch)(pattern, {
163+
__proto__: null,
164+
nocase: isWindows || isOSX,
165+
windowsPathsNoEscape: true,
166+
nonegate: true,
167+
nocomment: true,
168+
optimizationLevel: 2,
169+
platform: process.platform,
170+
nocaseMagicOnly: true,
171+
}));
172+
}
173+
174+
globSync() {
175+
ArrayPrototypePush(this.#queue, {
176+
__proto__: null,
177+
path: '.',
178+
patterns: ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set,
179+
(pattern, i) => new Pattern(
180+
pattern,
181+
matcher.globParts[i],
182+
new SafeSet([0]),
183+
new SafeSet(),
184+
))),
185+
});
186+
187+
while (this.#queue.length > 0) {
188+
const item = ArrayPrototypePop(this.#queue);
189+
for (let i = 0; i < item.patterns.length; i++) {
190+
this.#addSubpatterns(item.path, item.patterns[i]);
191+
}
192+
this.#subpatterns
193+
.forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns }));
194+
this.#subpatterns.clear();
195+
}
196+
return this.#results;
197+
}
198+
#addSubpattern(path, pattern) {
199+
if (!this.#subpatterns.has(path)) {
200+
this.#subpatterns.set(path, [pattern]);
201+
} else {
202+
ArrayPrototypePush(this.#subpatterns.get(path), pattern);
203+
}
204+
}
205+
#addSubpatterns(path, pattern) {
206+
const seen = this.#cache.add(path, pattern);
207+
if (seen) {
208+
return;
209+
}
210+
const fullpath = resolve(this.#root, path);
211+
const stat = this.#cache.statSync(fullpath);
212+
const last = pattern.last;
213+
const isDirectory = stat?.isDirectory() || (stat?.isSymbolicLink() && pattern.hasSeenSymlinks);
214+
const isLast = pattern.isLast(isDirectory);
215+
const isFirst = pattern.isFirst();
216+
217+
if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) {
218+
// Absolute path, go to root
219+
this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet([1])));
220+
return;
221+
}
222+
if (isFirst && pattern.at(0) === '') {
223+
// Absolute path, go to root
224+
this.#addSubpattern('/', pattern.child(new SafeSet([1])));
225+
return;
226+
}
227+
if (isFirst && pattern.at(0) === '..') {
228+
// Start with .., go to parent
229+
this.#addSubpattern('../', pattern.child(new SafeSet([1])));
230+
return;
231+
}
232+
if (isFirst && pattern.at(0) === '.') {
233+
// Start with ., proceed
234+
this.#addSubpattern('.', pattern.child(new SafeSet([1])));
235+
return;
236+
}
237+
238+
if (isLast && typeof pattern.at(-1) === 'string') {
239+
// Add result if it exists
240+
const p = pattern.at(-1);
241+
const stat = this.#cache.statSync(join(fullpath, p));
242+
if (stat && (p || isDirectory)) {
243+
ArrayPrototypePush(this.#results, join(path, p));
244+
}
245+
if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
246+
return;
247+
}
248+
} else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR &&
249+
(path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) {
250+
// If pattern ends with **, add to results
251+
// if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
252+
ArrayPrototypePush(this.#results, path);
253+
}
254+
255+
if (!isDirectory) {
256+
return;
257+
}
258+
259+
let children;
260+
const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value);
261+
if (typeof firstPattern === 'string') {
262+
const stat = this.#cache.statSync(join(fullpath, firstPattern));
263+
if (stat) {
264+
stat.name = firstPattern;
265+
children = [stat];
266+
} else {
267+
children = [];
268+
}
269+
} else {
270+
children = this.#cache.readdirSync(fullpath);
271+
}
272+
273+
for (let i = 0; i < children.length; i++) {
274+
const entry = children[i];
275+
const entryPath = join(path, entry.name);
276+
this.#cache.addToStatCache(join(fullpath, entry.name), entry);
277+
278+
const subPatterns = new SafeSet();
279+
const nSymlinks = new SafeSet();
280+
for (const index of pattern.indexes) {
281+
// For each child, chek potential patterns
282+
if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) {
283+
return;
284+
}
285+
const current = pattern.at(index);
286+
const nextIndex = index + 1;
287+
const next = pattern.at(nextIndex);
288+
const fromSymlink = pattern.symlinks.has(index);
289+
290+
if (current === lazyMinimatch().GLOBSTAR) {
291+
if (entry.name[0] === '.' || (this.#exclude && this.#exclude(entry.name))) {
292+
continue;
293+
}
294+
if (!fromSymlink && entry.isDirectory()) {
295+
// If directory, add ** to its potential patterns
296+
subPatterns.add(index);
297+
} else if (!fromSymlink && index === last) {
298+
// If ** is last, add to results
299+
ArrayPrototypePush(this.#results, entryPath);
300+
}
301+
302+
// Any pattern after ** is also a potential pattern
303+
// so we can already test it here
304+
const nextMatches = pattern.test(nextIndex, entry.name);
305+
if (nextMatches && nextIndex === last && !isLast) {
306+
// If next pattern is the last one, add to results
307+
ArrayPrototypePush(this.#results, entryPath);
308+
} else if (nextMatches && entry.isDirectory()) {
309+
// Pattern mached, meaning two patterns forward
310+
// are also potential patterns
311+
// e.g **/b/c when entry is a/b - add c to potential patterns
312+
subPatterns.add(index + 2);
313+
}
314+
if ((nextMatches || pattern.at(0) === '.') &&
315+
(entry.isDirectory() || entry.isSymbolicLink()) && !fromSymlink) {
316+
// If pattern after ** matches, or pattern starts with "."
317+
// and entry is a directory or symlink, add to potential patterns
318+
subPatterns.add(nextIndex);
319+
}
320+
321+
if (entry.isSymbolicLink()) {
322+
nSymlinks.add(index);
323+
}
324+
325+
if (next === '..' && entry.isDirectory()) {
326+
// In case pattern is "**/..",
327+
// both parent and current directory should be added to the queue
328+
// if this is the last pattern, add to results instead
329+
const parent = join(path, '..');
330+
if (nextIndex < last) {
331+
if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) {
332+
this.#subpatterns.set(path, [pattern.child(new SafeSet([nextIndex + 1]))]);
333+
}
334+
if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) {
335+
this.#subpatterns.set(parent, [pattern.child(new SafeSet([nextIndex + 1]))]);
336+
}
337+
} else {
338+
if (!this.#cache.seen(path, pattern, nextIndex)) {
339+
this.#cache.add(path, pattern.child(new SafeSet([nextIndex])));
340+
ArrayPrototypePush(this.#results, path);
341+
}
342+
if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) {
343+
this.#cache.add(parent, pattern.child(new SafeSet([nextIndex])));
344+
ArrayPrototypePush(this.#results, parent);
345+
}
346+
}
347+
}
348+
}
349+
if (typeof current === 'string') {
350+
if (pattern.test(index, entry.name) && index !== last) {
351+
// If current pattern matches entry name
352+
// the next pattern is a potential pattern
353+
subPatterns.add(nextIndex);
354+
} else if (current === '.' && pattern.test(nextIndex, entry.name)) {
355+
// If current pattern is ".", proceed to test next pattern
356+
if (nextIndex === last) {
357+
ArrayPrototypePush(this.#results, entryPath);
358+
} else {
359+
subPatterns.add(nextIndex + 1);
360+
}
361+
}
362+
}
363+
if (typeof current === 'object' && pattern.test(index, entry.name)) {
364+
// If current pattern is a regex that matches entry name (e.g *.js)
365+
// add next pattern to potential patterns, or to results if it's the last pattern
366+
if (index === last) {
367+
ArrayPrototypePush(this.#results, entryPath);
368+
} else if (entry.isDirectory()) {
369+
subPatterns.add(nextIndex);
370+
}
371+
}
372+
}
373+
if (subPatterns.size > 0) {
374+
// If there are potential patterns, add to queue
375+
this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks));
376+
}
377+
}
378+
}
379+
}
380+
381+
module.exports = {
382+
__proto__: null,
383+
Glob,
384+
};

0 commit comments

Comments
 (0)