UNPKG

6.59 kBJavaScriptView Raw
1import {readdir, stat, lstat} from "node:fs/promises";
2import {readdirSync, statSync, lstatSync} from "node:fs";
3import {sep, resolve} from "node:path";
4import picomatch from "picomatch";
5
6const encoder = new TextEncoder();
7const toUint8Array = encoder.encode.bind(encoder);
8const decoder = new TextDecoder();
9const toString = decoder.decode.bind(decoder);
10const sepUint8Array = toUint8Array(sep);
11
12const defaults = {
13 strict: false,
14 stats: false,
15 followSymlinks: false,
16 exclude: undefined,
17 include: undefined,
18 insensitive: false,
19};
20
21function makePath(entry, dir, encoding) {
22 if (encoding === "buffer") {
23 return dir === "." ? entry.name : Uint8Array.from([...dir, ...sepUint8Array, ...entry.name]);
24 } else {
25 return dir === "." ? entry.name : `${dir}${sep}${entry.name}`;
26 }
27}
28
29function build(dirent, path, stats, opts) {
30 return {
31 path,
32 directory: (stats || dirent).isDirectory(),
33 symlink: (stats || dirent).isSymbolicLink(),
34 ...(opts.stats ? {stats} : {}),
35 };
36}
37
38function makeMatchers({include, exclude, insensitive}) {
39 const opts = {
40 dot: true,
41 flags: insensitive ? "i" : undefined,
42 };
43
44 // resolve the path to an absolute one because picomatch can not deal properly
45 // with relative paths that start with ./ or .\
46 // https://github.com/micromatch/picomatch/issues/121
47 return {
48 includeMatcher: include?.length ? path => picomatch(include, opts)(resolve(path)) : null,
49 excludeMatcher: exclude?.length ? path => picomatch(exclude, opts)(resolve(path)) : null,
50 };
51}
52
53export async function* rrdir(dir, opts = {}, {includeMatcher, excludeMatcher, encoding} = {}) {
54 if (includeMatcher === undefined) {
55 opts = {...defaults, ...opts};
56 ({includeMatcher, excludeMatcher} = makeMatchers(opts));
57 if (/[/\\]$/.test(dir)) dir = dir.substring(0, dir.length - 1);
58 encoding = dir instanceof Uint8Array ? "buffer" : undefined;
59 }
60
61 let dirents = [];
62 try {
63 dirents = await readdir(dir, {encoding, withFileTypes: true});
64 } catch (err) {
65 if (opts.strict) throw err;
66 yield {path: dir, err};
67 }
68 if (!dirents.length) return;
69
70 for (const dirent of dirents) {
71 const path = makePath(dirent, dir, encoding);
72 if (excludeMatcher?.(encoding === "buffer" ? toString(path) : path)) continue;
73
74 const isSymbolicLink = opts.followSymlinks && dirent.isSymbolicLink();
75 const encodedPath = encoding === "buffer" ? toString(path) : path;
76 const isIncluded = !includeMatcher || includeMatcher(encodedPath);
77 let stats;
78
79 if (isIncluded) {
80 if (opts.stats || isSymbolicLink) {
81 try {
82 stats = await (opts.followSymlinks ? stat : lstat)(path);
83 } catch (err) {
84 if (opts.strict) throw err;
85 yield {path, err};
86 }
87 }
88
89 yield build(dirent, path, stats, opts);
90 }
91
92 let recurse = false;
93 if (isSymbolicLink) {
94 if (!stats) try { stats = await stat(path); } catch {}
95 if (stats && stats.isDirectory()) recurse = true;
96 } else if (dirent.isDirectory()) {
97 recurse = true;
98 }
99
100 if (recurse) yield* await rrdir(path, opts, {includeMatcher, excludeMatcher, encoding});
101 }
102}
103
104export async function rrdirAsync(dir, opts = {}, {includeMatcher, excludeMatcher, encoding} = {}) {
105 if (includeMatcher === undefined) {
106 opts = {...defaults, ...opts};
107 ({includeMatcher, excludeMatcher} = makeMatchers(opts));
108 if (/[/\\]$/.test(dir)) dir = dir.substring(0, dir.length - 1);
109 encoding = dir instanceof Uint8Array ? "buffer" : undefined;
110 }
111
112 const results = [];
113 let dirents = [];
114 try {
115 dirents = await readdir(dir, {encoding, withFileTypes: true});
116 } catch (err) {
117 if (opts.strict) throw err;
118 results.push({path: dir, err});
119 }
120 if (!dirents.length) return results;
121
122 await Promise.all(dirents.map(async dirent => {
123 const path = makePath(dirent, dir, encoding);
124 if (excludeMatcher?.(encoding === "buffer" ? toString(path) : path)) return;
125
126 const isSymbolicLink = opts.followSymlinks && dirent.isSymbolicLink();
127 const encodedPath = encoding === "buffer" ? toString(path) : path;
128 const isIncluded = !includeMatcher || includeMatcher(encodedPath);
129 let stats;
130
131 if (isIncluded) {
132 if (opts.stats || isSymbolicLink) {
133 try {
134 stats = await (opts.followSymlinks ? stat : lstat)(path);
135 } catch (err) {
136 if (opts.strict) throw err;
137 results.push({path, err});
138 }
139 }
140
141 results.push(build(dirent, path, stats, opts));
142 }
143
144 let recurse = false;
145 if (isSymbolicLink) {
146 if (!stats) try { stats = await stat(path); } catch {}
147 if (stats && stats.isDirectory()) recurse = true;
148 } else if (dirent.isDirectory()) {
149 recurse = true;
150 }
151
152 if (recurse) results.push(...await rrdirAsync(path, opts, {includeMatcher, excludeMatcher, encoding}));
153 }));
154
155 return results;
156}
157
158export function rrdirSync(dir, opts = {}, {includeMatcher, excludeMatcher, encoding} = {}) {
159 if (includeMatcher === undefined) {
160 opts = {...defaults, ...opts};
161 ({includeMatcher, excludeMatcher} = makeMatchers(opts));
162 if (/[/\\]$/.test(dir)) dir = dir.substring(0, dir.length - 1);
163 encoding = dir instanceof Uint8Array ? "buffer" : undefined;
164 }
165
166 const results = [];
167 let dirents = [];
168 try {
169 dirents = readdirSync(dir, {encoding, withFileTypes: true});
170 } catch (err) {
171 if (opts.strict) throw err;
172 results.push({path: dir, err});
173 }
174 if (!dirents.length) return results;
175
176 for (const dirent of dirents) {
177 const path = makePath(dirent, dir, encoding);
178 if (excludeMatcher?.(encoding === "buffer" ? toString(path) : path)) continue;
179
180 const isSymbolicLink = opts.followSymlinks && dirent.isSymbolicLink();
181 const encodedPath = encoding === "buffer" ? toString(path) : path;
182 const isIncluded = !includeMatcher || includeMatcher(encodedPath);
183 let stats;
184
185 if (isIncluded) {
186 if (opts.stats || isSymbolicLink) {
187 try {
188 stats = (opts.followSymlinks ? statSync : lstatSync)(path);
189 } catch (err) {
190 if (opts.strict) throw err;
191 results.push({path, err});
192 }
193 }
194 results.push(build(dirent, path, stats, opts));
195 }
196
197 let recurse = false;
198 if (isSymbolicLink) {
199 if (!stats) try { stats = statSync(path); } catch {}
200 if (stats && stats.isDirectory()) recurse = true;
201 } else if (dirent.isDirectory()) {
202 recurse = true;
203 }
204
205 if (recurse) results.push(...rrdirSync(path, opts, {includeMatcher, excludeMatcher, encoding}));
206 }
207
208 return results;
209}