UNPKG

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