walker.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. /**
  2. * Single-use utility classes to provide functionality to the {@link Glob}
  3. * methods.
  4. *
  5. * @module
  6. */
  7. import { Minipass } from 'minipass';
  8. import { Ignore } from './ignore.js';
  9. import { Processor } from './processor.js';
  10. const makeIgnore = (ignore, opts) => typeof ignore === 'string' ? new Ignore([ignore], opts)
  11. : Array.isArray(ignore) ? new Ignore(ignore, opts)
  12. : ignore;
  13. /**
  14. * basic walking utilities that all the glob walker types use
  15. */
  16. export class GlobUtil {
  17. path;
  18. patterns;
  19. opts;
  20. seen = new Set();
  21. paused = false;
  22. aborted = false;
  23. #onResume = [];
  24. #ignore;
  25. #sep;
  26. signal;
  27. maxDepth;
  28. includeChildMatches;
  29. constructor(patterns, path, opts) {
  30. this.patterns = patterns;
  31. this.path = path;
  32. this.opts = opts;
  33. this.#sep = !opts.posix && opts.platform === 'win32' ? '\\' : '/';
  34. this.includeChildMatches = opts.includeChildMatches !== false;
  35. if (opts.ignore || !this.includeChildMatches) {
  36. this.#ignore = makeIgnore(opts.ignore ?? [], opts);
  37. if (!this.includeChildMatches &&
  38. typeof this.#ignore.add !== 'function') {
  39. const m = 'cannot ignore child matches, ignore lacks add() method.';
  40. throw new Error(m);
  41. }
  42. }
  43. // ignore, always set with maxDepth, but it's optional on the
  44. // GlobOptions type
  45. /* c8 ignore start */
  46. this.maxDepth = opts.maxDepth || Infinity;
  47. /* c8 ignore stop */
  48. if (opts.signal) {
  49. this.signal = opts.signal;
  50. this.signal.addEventListener('abort', () => {
  51. this.#onResume.length = 0;
  52. });
  53. }
  54. }
  55. #ignored(path) {
  56. return this.seen.has(path) || !!this.#ignore?.ignored?.(path);
  57. }
  58. #childrenIgnored(path) {
  59. return !!this.#ignore?.childrenIgnored?.(path);
  60. }
  61. // backpressure mechanism
  62. pause() {
  63. this.paused = true;
  64. }
  65. resume() {
  66. /* c8 ignore start */
  67. if (this.signal?.aborted)
  68. return;
  69. /* c8 ignore stop */
  70. this.paused = false;
  71. let fn = undefined;
  72. while (!this.paused && (fn = this.#onResume.shift())) {
  73. fn();
  74. }
  75. }
  76. onResume(fn) {
  77. if (this.signal?.aborted)
  78. return;
  79. /* c8 ignore start */
  80. if (!this.paused) {
  81. fn();
  82. }
  83. else {
  84. /* c8 ignore stop */
  85. this.#onResume.push(fn);
  86. }
  87. }
  88. // do the requisite realpath/stat checking, and return the path
  89. // to add or undefined to filter it out.
  90. async matchCheck(e, ifDir) {
  91. if (ifDir && this.opts.nodir)
  92. return undefined;
  93. let rpc;
  94. if (this.opts.realpath) {
  95. rpc = e.realpathCached() || (await e.realpath());
  96. if (!rpc)
  97. return undefined;
  98. e = rpc;
  99. }
  100. const needStat = e.isUnknown() || this.opts.stat;
  101. const s = needStat ? await e.lstat() : e;
  102. if (this.opts.follow && this.opts.nodir && s?.isSymbolicLink()) {
  103. const target = await s.realpath();
  104. /* c8 ignore start */
  105. if (target && (target.isUnknown() || this.opts.stat)) {
  106. await target.lstat();
  107. }
  108. /* c8 ignore stop */
  109. }
  110. return this.matchCheckTest(s, ifDir);
  111. }
  112. matchCheckTest(e, ifDir) {
  113. return (e &&
  114. (this.maxDepth === Infinity || e.depth() <= this.maxDepth) &&
  115. (!ifDir || e.canReaddir()) &&
  116. (!this.opts.nodir || !e.isDirectory()) &&
  117. (!this.opts.nodir ||
  118. !this.opts.follow ||
  119. !e.isSymbolicLink() ||
  120. !e.realpathCached()?.isDirectory()) &&
  121. !this.#ignored(e)) ?
  122. e
  123. : undefined;
  124. }
  125. matchCheckSync(e, ifDir) {
  126. if (ifDir && this.opts.nodir)
  127. return undefined;
  128. let rpc;
  129. if (this.opts.realpath) {
  130. rpc = e.realpathCached() || e.realpathSync();
  131. if (!rpc)
  132. return undefined;
  133. e = rpc;
  134. }
  135. const needStat = e.isUnknown() || this.opts.stat;
  136. const s = needStat ? e.lstatSync() : e;
  137. if (this.opts.follow && this.opts.nodir && s?.isSymbolicLink()) {
  138. const target = s.realpathSync();
  139. if (target && (target?.isUnknown() || this.opts.stat)) {
  140. target.lstatSync();
  141. }
  142. }
  143. return this.matchCheckTest(s, ifDir);
  144. }
  145. matchFinish(e, absolute) {
  146. if (this.#ignored(e))
  147. return;
  148. // we know we have an ignore if this is false, but TS doesn't
  149. if (!this.includeChildMatches && this.#ignore?.add) {
  150. const ign = `${e.relativePosix()}/**`;
  151. this.#ignore.add(ign);
  152. }
  153. const abs = this.opts.absolute === undefined ? absolute : this.opts.absolute;
  154. this.seen.add(e);
  155. const mark = this.opts.mark && e.isDirectory() ? this.#sep : '';
  156. // ok, we have what we need!
  157. if (this.opts.withFileTypes) {
  158. this.matchEmit(e);
  159. }
  160. else if (abs) {
  161. const abs = this.opts.posix ? e.fullpathPosix() : e.fullpath();
  162. this.matchEmit(abs + mark);
  163. }
  164. else {
  165. const rel = this.opts.posix ? e.relativePosix() : e.relative();
  166. const pre = this.opts.dotRelative && !rel.startsWith('..' + this.#sep) ?
  167. '.' + this.#sep
  168. : '';
  169. this.matchEmit(!rel ? '.' + mark : pre + rel + mark);
  170. }
  171. }
  172. async match(e, absolute, ifDir) {
  173. const p = await this.matchCheck(e, ifDir);
  174. if (p)
  175. this.matchFinish(p, absolute);
  176. }
  177. matchSync(e, absolute, ifDir) {
  178. const p = this.matchCheckSync(e, ifDir);
  179. if (p)
  180. this.matchFinish(p, absolute);
  181. }
  182. walkCB(target, patterns, cb) {
  183. /* c8 ignore start */
  184. if (this.signal?.aborted)
  185. cb();
  186. /* c8 ignore stop */
  187. this.walkCB2(target, patterns, new Processor(this.opts), cb);
  188. }
  189. walkCB2(target, patterns, processor, cb) {
  190. if (this.#childrenIgnored(target))
  191. return cb();
  192. if (this.signal?.aborted)
  193. cb();
  194. if (this.paused) {
  195. this.onResume(() => this.walkCB2(target, patterns, processor, cb));
  196. return;
  197. }
  198. processor.processPatterns(target, patterns);
  199. // done processing. all of the above is sync, can be abstracted out.
  200. // subwalks is a map of paths to the entry filters they need
  201. // matches is a map of paths to [absolute, ifDir] tuples.
  202. let tasks = 1;
  203. const next = () => {
  204. if (--tasks === 0)
  205. cb();
  206. };
  207. for (const [m, absolute, ifDir] of processor.matches.entries()) {
  208. if (this.#ignored(m))
  209. continue;
  210. tasks++;
  211. this.match(m, absolute, ifDir).then(() => next());
  212. }
  213. for (const t of processor.subwalkTargets()) {
  214. if (this.maxDepth !== Infinity && t.depth() >= this.maxDepth) {
  215. continue;
  216. }
  217. tasks++;
  218. const childrenCached = t.readdirCached();
  219. if (t.calledReaddir())
  220. this.walkCB3(t, childrenCached, processor, next);
  221. else {
  222. t.readdirCB((_, entries) => this.walkCB3(t, entries, processor, next), true);
  223. }
  224. }
  225. next();
  226. }
  227. walkCB3(target, entries, processor, cb) {
  228. processor = processor.filterEntries(target, entries);
  229. let tasks = 1;
  230. const next = () => {
  231. if (--tasks === 0)
  232. cb();
  233. };
  234. for (const [m, absolute, ifDir] of processor.matches.entries()) {
  235. if (this.#ignored(m))
  236. continue;
  237. tasks++;
  238. this.match(m, absolute, ifDir).then(() => next());
  239. }
  240. for (const [target, patterns] of processor.subwalks.entries()) {
  241. tasks++;
  242. this.walkCB2(target, patterns, processor.child(), next);
  243. }
  244. next();
  245. }
  246. walkCBSync(target, patterns, cb) {
  247. /* c8 ignore start */
  248. if (this.signal?.aborted)
  249. cb();
  250. /* c8 ignore stop */
  251. this.walkCB2Sync(target, patterns, new Processor(this.opts), cb);
  252. }
  253. walkCB2Sync(target, patterns, processor, cb) {
  254. if (this.#childrenIgnored(target))
  255. return cb();
  256. if (this.signal?.aborted)
  257. cb();
  258. if (this.paused) {
  259. this.onResume(() => this.walkCB2Sync(target, patterns, processor, cb));
  260. return;
  261. }
  262. processor.processPatterns(target, patterns);
  263. // done processing. all of the above is sync, can be abstracted out.
  264. // subwalks is a map of paths to the entry filters they need
  265. // matches is a map of paths to [absolute, ifDir] tuples.
  266. let tasks = 1;
  267. const next = () => {
  268. if (--tasks === 0)
  269. cb();
  270. };
  271. for (const [m, absolute, ifDir] of processor.matches.entries()) {
  272. if (this.#ignored(m))
  273. continue;
  274. this.matchSync(m, absolute, ifDir);
  275. }
  276. for (const t of processor.subwalkTargets()) {
  277. if (this.maxDepth !== Infinity && t.depth() >= this.maxDepth) {
  278. continue;
  279. }
  280. tasks++;
  281. const children = t.readdirSync();
  282. this.walkCB3Sync(t, children, processor, next);
  283. }
  284. next();
  285. }
  286. walkCB3Sync(target, entries, processor, cb) {
  287. processor = processor.filterEntries(target, entries);
  288. let tasks = 1;
  289. const next = () => {
  290. if (--tasks === 0)
  291. cb();
  292. };
  293. for (const [m, absolute, ifDir] of processor.matches.entries()) {
  294. if (this.#ignored(m))
  295. continue;
  296. this.matchSync(m, absolute, ifDir);
  297. }
  298. for (const [target, patterns] of processor.subwalks.entries()) {
  299. tasks++;
  300. this.walkCB2Sync(target, patterns, processor.child(), next);
  301. }
  302. next();
  303. }
  304. }
  305. export class GlobWalker extends GlobUtil {
  306. matches = new Set();
  307. constructor(patterns, path, opts) {
  308. super(patterns, path, opts);
  309. }
  310. matchEmit(e) {
  311. this.matches.add(e);
  312. }
  313. async walk() {
  314. if (this.signal?.aborted)
  315. throw this.signal.reason;
  316. if (this.path.isUnknown()) {
  317. await this.path.lstat();
  318. }
  319. await new Promise((res, rej) => {
  320. this.walkCB(this.path, this.patterns, () => {
  321. if (this.signal?.aborted) {
  322. rej(this.signal.reason);
  323. }
  324. else {
  325. res(this.matches);
  326. }
  327. });
  328. });
  329. return this.matches;
  330. }
  331. walkSync() {
  332. if (this.signal?.aborted)
  333. throw this.signal.reason;
  334. if (this.path.isUnknown()) {
  335. this.path.lstatSync();
  336. }
  337. // nothing for the callback to do, because this never pauses
  338. this.walkCBSync(this.path, this.patterns, () => {
  339. if (this.signal?.aborted)
  340. throw this.signal.reason;
  341. });
  342. return this.matches;
  343. }
  344. }
  345. export class GlobStream extends GlobUtil {
  346. results;
  347. constructor(patterns, path, opts) {
  348. super(patterns, path, opts);
  349. this.results = new Minipass({
  350. signal: this.signal,
  351. objectMode: true,
  352. });
  353. this.results.on('drain', () => this.resume());
  354. this.results.on('resume', () => this.resume());
  355. }
  356. matchEmit(e) {
  357. this.results.write(e);
  358. if (!this.results.flowing)
  359. this.pause();
  360. }
  361. stream() {
  362. const target = this.path;
  363. if (target.isUnknown()) {
  364. target.lstat().then(() => {
  365. this.walkCB(target, this.patterns, () => this.results.end());
  366. });
  367. }
  368. else {
  369. this.walkCB(target, this.patterns, () => this.results.end());
  370. }
  371. return this.results;
  372. }
  373. streamSync() {
  374. if (this.path.isUnknown()) {
  375. this.path.lstatSync();
  376. }
  377. this.walkCBSync(this.path, this.patterns, () => this.results.end());
  378. return this.results;
  379. }
  380. }
  381. //# sourceMappingURL=walker.js.map