filehound 2.0.0-beta.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +39 -11
  2. package/dist/arrays.d.ts +1 -0
  3. package/dist/arrays.js +4 -0
  4. package/dist/file.d.ts +108 -0
  5. package/dist/file.js +184 -0
  6. package/dist/filehound.d.ts +356 -0
  7. package/dist/filehound.js +498 -0
  8. package/dist/files.d.ts +4 -0
  9. package/dist/files.js +37 -0
  10. package/dist/filetype.d.ts +10 -0
  11. package/dist/filetype.js +9 -0
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.js +6 -0
  14. package/dist/matchers.d.ts +324 -0
  15. package/dist/matchers.js +419 -0
  16. package/dist/walker/async.d.ts +4 -0
  17. package/dist/walker/async.js +237 -0
  18. package/dist/walker/heap.d.ts +7 -0
  19. package/dist/walker/heap.js +49 -0
  20. package/dist/walker/sync.d.ts +4 -0
  21. package/dist/walker/sync.js +66 -0
  22. package/dist/walker/walk.d.ts +33 -0
  23. package/dist/walker/walk.js +80 -0
  24. package/package.json +45 -48
  25. package/.github/stale.yml +0 -42
  26. package/.travis.yml +0 -8
  27. package/.vscode/launch.json +0 -21
  28. package/.vscode/settings.json +0 -3
  29. package/CONTRIBUTING.md +0 -8
  30. package/_config.yml +0 -1
  31. package/coverage/coverage.raw.json +0 -1
  32. package/coverage/lcov-report/arrays.ts.html +0 -98
  33. package/coverage/lcov-report/base.css +0 -212
  34. package/coverage/lcov-report/filehound.ts.html +0 -2078
  35. package/coverage/lcov-report/files.ts.html +0 -260
  36. package/coverage/lcov-report/functions.ts.html +0 -122
  37. package/coverage/lcov-report/index.html +0 -132
  38. package/coverage/lcov-report/prettify.css +0 -1
  39. package/coverage/lcov-report/prettify.js +0 -1
  40. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  41. package/coverage/lcov-report/sorter.js +0 -158
  42. package/coverage/lcov.info +0 -378
  43. package/dist/src/arrays.js +0 -15
  44. package/dist/src/arrays.js.map +0 -1
  45. package/dist/src/asyncWalker.js +0 -59
  46. package/dist/src/asyncWalker.js.map +0 -1
  47. package/dist/src/bind.js +0 -15
  48. package/dist/src/bind.js.map +0 -1
  49. package/dist/src/filehound.js +0 -597
  50. package/dist/src/filehound.js.map +0 -1
  51. package/dist/src/files.js +0 -60
  52. package/dist/src/files.js.map +0 -1
  53. package/dist/src/matcher.js +0 -51
  54. package/dist/src/matcher.js.map +0 -1
  55. package/dist/src/syncWalker.js +0 -47
  56. package/dist/src/syncWalker.js.map +0 -1
  57. package/dist/src/walkAsync.js +0 -33
  58. package/dist/src/walkAsync.js.map +0 -1
  59. package/dist/src/walkSync.js +0 -21
  60. package/dist/src/walkSync.js.map +0 -1
  61. package/dist/src/walker/walk.js +0 -68
  62. package/dist/src/walker/walk.js.map +0 -1
  63. package/dist/src/walker/walkAsync.js +0 -33
  64. package/dist/src/walker/walkAsync.js.map +0 -1
  65. package/dist/src/walker/walkSync.js +0 -21
  66. package/dist/src/walker/walkSync.js.map +0 -1
  67. package/dist/test/filehound.js +0 -872
  68. package/dist/test/filehound.js.map +0 -1
  69. package/dist/test/files.js +0 -29
  70. package/dist/test/files.js.map +0 -1
  71. package/docconfig/jsdoc.json +0 -26
  72. package/docconfig/template/LICENSE +0 -61
  73. package/docconfig/template/README.md +0 -86
  74. package/docconfig/template/package.json +0 -22
  75. package/docconfig/template/publish.js +0 -676
  76. package/docconfig/template/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  77. package/docconfig/template/static/fonts/OpenSans-Bold-webfont.svg +0 -1830
  78. package/docconfig/template/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  79. package/docconfig/template/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  80. package/docconfig/template/static/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
  81. package/docconfig/template/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  82. package/docconfig/template/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  83. package/docconfig/template/static/fonts/OpenSans-Italic-webfont.svg +0 -1830
  84. package/docconfig/template/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  85. package/docconfig/template/static/fonts/OpenSans-Light-webfont.eot +0 -0
  86. package/docconfig/template/static/fonts/OpenSans-Light-webfont.svg +0 -1831
  87. package/docconfig/template/static/fonts/OpenSans-Light-webfont.woff +0 -0
  88. package/docconfig/template/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  89. package/docconfig/template/static/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
  90. package/docconfig/template/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  91. package/docconfig/template/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  92. package/docconfig/template/static/fonts/OpenSans-Regular-webfont.svg +0 -1831
  93. package/docconfig/template/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  94. package/docconfig/template/static/fonts/OpenSans-Semibold-webfont.eot +0 -0
  95. package/docconfig/template/static/fonts/OpenSans-Semibold-webfont.svg +0 -1830
  96. package/docconfig/template/static/fonts/OpenSans-Semibold-webfont.ttf +0 -0
  97. package/docconfig/template/static/fonts/OpenSans-Semibold-webfont.woff +0 -0
  98. package/docconfig/template/static/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
  99. package/docconfig/template/static/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
  100. package/docconfig/template/static/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
  101. package/docconfig/template/static/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
  102. package/docconfig/template/static/scripts/linenumber.js +0 -25
  103. package/docconfig/template/static/scripts/prettify/Apache-License-2.0.txt +0 -202
  104. package/docconfig/template/static/scripts/prettify/lang-css.js +0 -2
  105. package/docconfig/template/static/scripts/prettify/prettify.js +0 -28
  106. package/docconfig/template/static/styles/jsdoc-default.css +0 -605
  107. package/docconfig/template/static/styles/prettify-jsdoc.css +0 -111
  108. package/docconfig/template/static/styles/prettify-tomorrow.css +0 -132
  109. package/docconfig/template/tmpl/augments.tmpl +0 -10
  110. package/docconfig/template/tmpl/container.tmpl +0 -183
  111. package/docconfig/template/tmpl/details.tmpl +0 -143
  112. package/docconfig/template/tmpl/example.tmpl +0 -2
  113. package/docconfig/template/tmpl/examples.tmpl +0 -13
  114. package/docconfig/template/tmpl/exceptions.tmpl +0 -32
  115. package/docconfig/template/tmpl/layout.tmpl +0 -47
  116. package/docconfig/template/tmpl/mainpage.tmpl +0 -10
  117. package/docconfig/template/tmpl/members.tmpl +0 -38
  118. package/docconfig/template/tmpl/method.tmpl +0 -105
  119. package/docconfig/template/tmpl/params.tmpl +0 -124
  120. package/docconfig/template/tmpl/properties.tmpl +0 -108
  121. package/docconfig/template/tmpl/returns.tmpl +0 -19
  122. package/docconfig/template/tmpl/source.tmpl +0 -8
  123. package/docconfig/template/tmpl/tutorial.tmpl +0 -19
  124. package/docconfig/template/tmpl/type.tmpl +0 -7
  125. package/docs/FileHound.html +0 -3079
  126. package/docs/FileHound_modified.html +0 -245
  127. package/docs/_config.yml +0 -1
  128. package/docs/filehound.js.html +0 -697
  129. package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
  130. package/docs/fonts/OpenSans-Bold-webfont.svg +0 -1830
  131. package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
  132. package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  133. package/docs/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
  134. package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  135. package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
  136. package/docs/fonts/OpenSans-Italic-webfont.svg +0 -1830
  137. package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
  138. package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
  139. package/docs/fonts/OpenSans-Light-webfont.svg +0 -1831
  140. package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
  141. package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  142. package/docs/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
  143. package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  144. package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
  145. package/docs/fonts/OpenSans-Regular-webfont.svg +0 -1831
  146. package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
  147. package/docs/fonts/OpenSans-Semibold-webfont.eot +0 -0
  148. package/docs/fonts/OpenSans-Semibold-webfont.svg +0 -1830
  149. package/docs/fonts/OpenSans-Semibold-webfont.ttf +0 -0
  150. package/docs/fonts/OpenSans-Semibold-webfont.woff +0 -0
  151. package/docs/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
  152. package/docs/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
  153. package/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
  154. package/docs/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
  155. package/docs/global.html +0 -288
  156. package/docs/index.html +0 -154
  157. package/docs/scripts/linenumber.js +0 -25
  158. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
  159. package/docs/scripts/prettify/lang-css.js +0 -2
  160. package/docs/scripts/prettify/prettify.js +0 -28
  161. package/docs/styles/jsdoc-default.css +0 -605
  162. package/docs/styles/prettify-jsdoc.css +0 -111
  163. package/docs/styles/prettify-tomorrow.css +0 -132
  164. package/docs.md +0 -164
  165. package/index.js +0 -1
  166. package/issue_template.md +0 -6
  167. package/test/filehound.ts +0 -1076
  168. package/test/files.ts +0 -29
  169. package/test/fixtures/custom/failed.txt +0 -0
  170. package/test/fixtures/custom/passed.txt +0 -0
  171. package/test/fixtures/deeplyNested/c.json +0 -0
  172. package/test/fixtures/deeplyNested/d.json +0 -0
  173. package/test/fixtures/deeplyNested/mydir/e.json +0 -0
  174. package/test/fixtures/deeplyNested/mydir/mydir2/f.json +0 -0
  175. package/test/fixtures/deeplyNested/mydir/mydir2/mydir3/mydir4/failed.json +0 -0
  176. package/test/fixtures/deeplyNested/mydir/mydir2/mydir3/z.json +0 -0
  177. package/test/fixtures/deeplyNested/mydir/mydir2/y.json +0 -0
  178. package/test/fixtures/deeplyNestedWithHiddenDir/c.json +0 -0
  179. package/test/fixtures/deeplyNestedWithHiddenDir/d.json +0 -0
  180. package/test/fixtures/deeplyNestedWithHiddenDir/mydir/e.json +0 -0
  181. package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/f.json +0 -0
  182. package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/mydir3/mydir4/.mydir5/.keep +0 -0
  183. package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/mydir3/mydir4/failed.json +0 -0
  184. package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/mydir3/z.json +0 -0
  185. package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/y.json +0 -0
  186. package/test/fixtures/ext/Dummy.java +0 -0
  187. package/test/fixtures/ext/Dumpy.go +0 -0
  188. package/test/fixtures/ext/dummy.json +0 -0
  189. package/test/fixtures/ext/dummy.txt +0 -0
  190. package/test/fixtures/justFiles/a.json +0 -0
  191. package/test/fixtures/justFiles/b.json +0 -0
  192. package/test/fixtures/justFiles/dummy.txt +0 -0
  193. package/test/fixtures/mixed/a.json +0 -0
  194. package/test/fixtures/mixed/aabbcc.json +0 -0
  195. package/test/fixtures/mixed/ab.json +0 -0
  196. package/test/fixtures/mixed/z.json +0 -0
  197. package/test/fixtures/nested/.hidden1/bad.txt +0 -0
  198. package/test/fixtures/nested/c.json +0 -0
  199. package/test/fixtures/nested/d.json +0 -0
  200. package/test/fixtures/nested/mydir/e.json +0 -0
  201. package/test/fixtures/sizes/10b.txt +0 -0
  202. package/test/fixtures/sizes/1b.txt +0 -0
  203. package/test/fixtures/sizes/1k.txt +0 -0
  204. package/test/fixtures/sizes/2k.txt +0 -0
  205. package/test/fixtures/visibility/.hidden/.hidden.json +0 -0
  206. package/test/fixtures/visibility/.hidden/visible.json +0 -3
  207. package/test/fixtures/visibility/.hidden.json +0 -3
  208. package/test/fixtures/visibility/visible/.hidden.json +0 -3
  209. package/test/fixtures/visibility/visible.json +0 -3
  210. package/test/mocha.opts +0 -4
  211. package/tsconfig.json +0 -16
  212. package/tslint.json +0 -44
@@ -0,0 +1,419 @@
1
+ import { parseAge, parseSize } from 'unit-compare';
2
+ import { from } from './arrays.js';
3
+ // Brands predicates that test a file without I/O, so find() can call testSync
4
+ // and save a promise per file. The symbol is not exported from the package, so
5
+ // any other predicate is assumed to need I/O and is tested with test().
6
+ const sync = Symbol('SyncFilePredicate');
7
+ export function isSyncPredicate(predicate) {
8
+ return predicate[sync] === true;
9
+ }
10
+ // Predicates that need no I/O only implement testSync
11
+ export class AbstractFilePredicate {
12
+ or(f) {
13
+ return new Or(this, f);
14
+ }
15
+ and(f) {
16
+ return new And(this, f);
17
+ }
18
+ not() {
19
+ return new Not(this);
20
+ }
21
+ async test(file) {
22
+ return this.testSync(file);
23
+ }
24
+ }
25
+ export class And extends AbstractFilePredicate {
26
+ #left;
27
+ #right;
28
+ constructor(left, right) {
29
+ super();
30
+ this.#left = left;
31
+ this.#right = right;
32
+ }
33
+ get [sync]() {
34
+ return isSyncPredicate(this.#left) && isSyncPredicate(this.#right);
35
+ }
36
+ async test(file) {
37
+ return await this.#left.test(file) && this.#right.test(file);
38
+ }
39
+ testSync(file) {
40
+ return this.#left.testSync(file) && this.#right.testSync(file);
41
+ }
42
+ }
43
+ export class Not extends AbstractFilePredicate {
44
+ #filePredicate;
45
+ constructor(filePredicate) {
46
+ super();
47
+ this.#filePredicate = filePredicate;
48
+ }
49
+ get [sync]() {
50
+ return isSyncPredicate(this.#filePredicate);
51
+ }
52
+ async test(file) {
53
+ return !await this.#filePredicate.test(file);
54
+ }
55
+ testSync(file) {
56
+ return !this.#filePredicate.testSync(file);
57
+ }
58
+ }
59
+ export class Or extends AbstractFilePredicate {
60
+ #left;
61
+ #right;
62
+ constructor(left, right) {
63
+ super();
64
+ this.#left = left;
65
+ this.#right = right;
66
+ }
67
+ get [sync]() {
68
+ return isSyncPredicate(this.#left) && isSyncPredicate(this.#right);
69
+ }
70
+ async test(file) {
71
+ return await this.#left.test(file) || this.#right.test(file);
72
+ }
73
+ testSync(file) {
74
+ return this.#left.testSync(file) || this.#right.testSync(file);
75
+ }
76
+ }
77
+ export class Extension extends AbstractFilePredicate {
78
+ [sync] = true;
79
+ #extensions;
80
+ constructor(extensions) {
81
+ super();
82
+ this.#extensions = extensions;
83
+ }
84
+ testSync(file) {
85
+ return this.#extensions.includes(file.getPathExtension());
86
+ }
87
+ }
88
+ export class Glob extends AbstractFilePredicate {
89
+ [sync] = true;
90
+ #globPattern;
91
+ constructor(globPattern) {
92
+ super();
93
+ this.#globPattern = globPattern;
94
+ }
95
+ testSync(file) {
96
+ return file.isMatch(this.#globPattern);
97
+ }
98
+ }
99
+ export class TextMatch extends AbstractFilePredicate {
100
+ [sync] = true;
101
+ #pattern;
102
+ #flags;
103
+ constructor(pattern, flags) {
104
+ super();
105
+ this.#pattern = pattern;
106
+ this.#flags = flags;
107
+ }
108
+ testSync(file) {
109
+ return new RegExp(this.#pattern, this.#flags).test(file.getName());
110
+ }
111
+ }
112
+ export class CustomFilter extends AbstractFilePredicate {
113
+ #fn;
114
+ constructor(fn) {
115
+ super();
116
+ this.#fn = fn;
117
+ }
118
+ async test(file) {
119
+ return this.#fn(file);
120
+ }
121
+ testSync(_file) {
122
+ throw new TypeError('applyFilter() filters can only be used with find(). Use applyFilterSync() with findSync()');
123
+ }
124
+ }
125
+ export class CustomFilterSync extends AbstractFilePredicate {
126
+ [sync] = true;
127
+ #fn;
128
+ constructor(fn) {
129
+ super();
130
+ this.#fn = fn;
131
+ }
132
+ testSync(file) {
133
+ return this.#fn(file);
134
+ }
135
+ }
136
+ export class Size extends AbstractFilePredicate {
137
+ #isMatch;
138
+ constructor(sizeExpression) {
139
+ super();
140
+ this.#isMatch = parseSize(sizeExpression);
141
+ }
142
+ async test(file) {
143
+ return this.#isMatch(await file.size());
144
+ }
145
+ testSync(file) {
146
+ return this.#isMatch(file.sizeSync());
147
+ }
148
+ }
149
+ // Ages are measured when a file is tested, not when the predicate is made, so
150
+ // a predicate kept and reused later still compares against the current time
151
+ const MEASURED_WHEN_TESTED = Object.freeze({ now: () => Date.now() });
152
+ export class Modified extends AbstractFilePredicate {
153
+ #isMatch;
154
+ constructor(expression) {
155
+ super();
156
+ this.#isMatch = parseAge(expression, MEASURED_WHEN_TESTED);
157
+ }
158
+ async test(file) {
159
+ return this.#isMatch(await file.lastModified());
160
+ }
161
+ testSync(file) {
162
+ return this.#isMatch(file.lastModifiedSync());
163
+ }
164
+ }
165
+ export class Accessed extends AbstractFilePredicate {
166
+ #isMatch;
167
+ constructor(expression) {
168
+ super();
169
+ this.#isMatch = parseAge(expression, MEASURED_WHEN_TESTED);
170
+ }
171
+ async test(file) {
172
+ return this.#isMatch(await file.lastAccessed());
173
+ }
174
+ testSync(file) {
175
+ return this.#isMatch(file.lastAccessedSync());
176
+ }
177
+ }
178
+ export class Changed extends AbstractFilePredicate {
179
+ #isMatch;
180
+ constructor(expression) {
181
+ super();
182
+ this.#isMatch = parseAge(expression, MEASURED_WHEN_TESTED);
183
+ }
184
+ async test(file) {
185
+ return this.#isMatch(await file.lastChanged());
186
+ }
187
+ testSync(file) {
188
+ return this.#isMatch(file.lastChangedSync());
189
+ }
190
+ }
191
+ export class MatchAll extends AbstractFilePredicate {
192
+ [sync] = true;
193
+ testSync(_file) {
194
+ return true;
195
+ }
196
+ }
197
+ export function ext(...args) {
198
+ const extensions = from(args).map(cleanExtension);
199
+ return new Extension(extensions);
200
+ }
201
+ /**
202
+ * Matches every file
203
+ *
204
+ * @returns the predicate
205
+ */
206
+ export function matchAll() {
207
+ return new MatchAll();
208
+ }
209
+ // The matchers are typed, but JavaScript callers could still pass a number
210
+ // where V1 accepted one, so they're told what to use instead
211
+ function expressionFor(matcher, expression, example) {
212
+ if (typeof expression !== 'string') {
213
+ throw new TypeError(`${matcher}() takes an expression, such as '${example}'`);
214
+ }
215
+ return expression;
216
+ }
217
+ /**
218
+ * Matches files by size, using an expression of an optional operator, a
219
+ * whole number and an optional unit, such as `'>10kb'` or `'<= 1 mb'`.
220
+ *
221
+ * The operators are `<`, `<=`, `>`, `>=` and `==` (or `=`), which is the
222
+ * default. The units, in multiples of 1024, are bytes (`b`, the default),
223
+ * kilobytes (`k`, `kb`), megabytes (`m`, `mb`), gigabytes (`g`, `gb`) and
224
+ * terabytes (`t`, `tb`), in any case. Use {@link sizeEquals} for an exact
225
+ * number of bytes.
226
+ *
227
+ * @param expression - the size expression
228
+ * @returns the predicate
229
+ * @throws `TypeError`
230
+ * Thrown if the expression is not a string.
231
+ * @throws `ExpressionError`
232
+ * Thrown if the expression can't be parsed or has an unknown unit.
233
+ * @example
234
+ * ```ts
235
+ * FileHound.create().match(size('>1mb'));
236
+ * ```
237
+ */
238
+ export function size(expression) {
239
+ return new Size(expressionFor('size', expression, '>1kb'));
240
+ }
241
+ /**
242
+ * Matches files of an exact size
243
+ *
244
+ * @param bytes - the size in bytes
245
+ * @returns the predicate
246
+ * @throws `RangeError`
247
+ * Thrown if bytes is negative or not an integer.
248
+ * @example
249
+ * ```ts
250
+ * FileHound.create().match(sizeEquals(1024));
251
+ * ```
252
+ */
253
+ export function sizeEquals(bytes) {
254
+ if (!Number.isSafeInteger(bytes) || bytes < 0) {
255
+ throw new RangeError('Size must be a non-negative integer number of bytes');
256
+ }
257
+ return new Size(bytes);
258
+ }
259
+ /**
260
+ * Matches files with a size of zero bytes
261
+ *
262
+ * @returns the predicate
263
+ */
264
+ export function isEmpty() {
265
+ return new Size(0);
266
+ }
267
+ /**
268
+ * Matches files whose path, relative to the search path, satisfies a glob
269
+ * pattern. Patterns follow Node's `path.matchesGlob` and `fs.glob` rules:
270
+ * `*.json` matches only the top level of the search path, and a leading `**`
271
+ * segment matches at any depth. Wildcards, including `**`, do not match names
272
+ * beginning with a dot.
273
+ *
274
+ * @param globPattern - the glob pattern
275
+ * @returns the predicate
276
+ * @example
277
+ * ```ts
278
+ * FileHound.create().match(glob('*.json'));
279
+ * ```
280
+ */
281
+ export function glob(globPattern) {
282
+ return new Glob(globPattern);
283
+ }
284
+ /**
285
+ * Matches files using a custom function, which may be asynchronous. Can only
286
+ * be used with {@link FileHound.find}; use {@link applyFilterSync} with
287
+ * {@link FileHound.findSync}.
288
+ *
289
+ * @param fn - returns, or fulfils with, true for files to include
290
+ * @returns the predicate
291
+ * @example
292
+ * ```ts
293
+ * FileHound.create().match(applyFilter(async file => await file.size() > 0));
294
+ * ```
295
+ */
296
+ export function applyFilter(fn) {
297
+ return new CustomFilter(fn);
298
+ }
299
+ /**
300
+ * Matches files using a synchronous custom function. Can be used with both
301
+ * {@link FileHound.find} and {@link FileHound.findSync}.
302
+ *
303
+ * @param fn - returns true for files to include
304
+ * @returns the predicate
305
+ * @example
306
+ * ```ts
307
+ * FileHound.create().match(applyFilterSync(file => file.sizeSync() > 0));
308
+ * ```
309
+ */
310
+ export function applyFilterSync(fn) {
311
+ return new CustomFilterSync(fn);
312
+ }
313
+ /**
314
+ * Matches files whose path satisfies a regular expression pattern
315
+ *
316
+ * @param pattern - the regular expression pattern
317
+ * @returns the predicate
318
+ */
319
+ export function like(pattern) {
320
+ return new TextMatch(pattern);
321
+ }
322
+ /**
323
+ * Excludes files whose path satisfies any of the given regular expression patterns
324
+ *
325
+ * @remarks
326
+ * Used with {@link FileHound.match}, a discarded directory is left out of the
327
+ * results but still searched. To keep the search out of it, pass its name
328
+ * to {@link FileHound.skipDirectories} instead.
329
+ *
330
+ * @param patterns - one or more regular expression patterns
331
+ * @returns the predicate
332
+ * @example
333
+ * ```ts
334
+ * FileHound.create().match(discard('node_modules', '\\.git'));
335
+ * ```
336
+ */
337
+ export function discard(...patterns) {
338
+ if (patterns.length === 1) {
339
+ return new TextMatch(patterns[0]).not();
340
+ }
341
+ return patterns
342
+ .map((pattern) => new TextMatch(pattern))
343
+ .reduce((a, b) => a.or(b))
344
+ .not();
345
+ }
346
+ function cleanExtension(fileExtension) {
347
+ if (fileExtension.startsWith('.')) {
348
+ return fileExtension.slice(1);
349
+ }
350
+ return fileExtension;
351
+ }
352
+ /**
353
+ * Matches files by when they were last modified, using an expression of their
354
+ * age: an optional operator, a whole number and an optional unit, such as
355
+ * `'<2 days'`, `'>8h'` or `'>=1 week'`.
356
+ *
357
+ * The operators are `<`, `<=`, `>`, `>=` and `==` (or `=`), which is the
358
+ * default. The units are minutes (`m`, `min`, `mins`, `minute`, `minutes`),
359
+ * hours (`h`, `hour`, `hours`), days (`d`, `day`, `days`, the default) and
360
+ * weeks (`w`, `week`, `weeks`), in any case.
361
+ *
362
+ * @remarks
363
+ * Ages are compared in whole units, like `find -mtime`: a file modified two
364
+ * and a half days ago is 2 days old, so it matches `'=2 days'`, `'>=2 days'`
365
+ * and `'<3 days'`, but not `'>2 days'`. Days and weeks are calendar days, so
366
+ * stay correct across daylight saving changes. Ages are measured when each
367
+ * file is tested.
368
+ *
369
+ * @param expression - the age expression
370
+ * @returns the predicate
371
+ * @throws `TypeError`
372
+ * Thrown if the expression is not a string.
373
+ * @throws `ExpressionError`
374
+ * Thrown if the expression can't be parsed or has an unknown unit.
375
+ * @example
376
+ * ```ts
377
+ * FileHound.create().match(modified('<2 days'));
378
+ * ```
379
+ */
380
+ export function modified(expression) {
381
+ return new Modified(expressionFor('modified', expression, '<2 days'));
382
+ }
383
+ /**
384
+ * Matches files by when they were last accessed, using an age expression as
385
+ * for {@link modified}
386
+ *
387
+ * @param expression - the age expression
388
+ * @returns the predicate
389
+ * @throws `TypeError`
390
+ * Thrown if the expression is not a string.
391
+ * @throws `ExpressionError`
392
+ * Thrown if the expression can't be parsed or has an unknown unit.
393
+ */
394
+ export function accessed(expression) {
395
+ return new Accessed(expressionFor('accessed', expression, '<2 days'));
396
+ }
397
+ /**
398
+ * Matches files by when their status, such as their permissions, last
399
+ * changed, using an age expression as for {@link modified}
400
+ *
401
+ * @param expression - the age expression
402
+ * @returns the predicate
403
+ * @throws `TypeError`
404
+ * Thrown if the expression is not a string.
405
+ * @throws `ExpressionError`
406
+ * Thrown if the expression can't be parsed or has an unknown unit.
407
+ */
408
+ export function changed(expression) {
409
+ return new Changed(expressionFor('changed', expression, '<2 days'));
410
+ }
411
+ /**
412
+ * Negates a predicate
413
+ *
414
+ * @param fn - the predicate to negate
415
+ * @returns a predicate that matches when the given one does not
416
+ */
417
+ export function not(fn) {
418
+ return new Not(fn);
419
+ }
@@ -0,0 +1,4 @@
1
+ import { File } from '../file.js';
2
+ import type { FilePredicate } from '../matchers.js';
3
+ import { type WalkOptions } from './walk.js';
4
+ export declare function walkAsync(dir: string, matcher: FilePredicate, opts: WalkOptions): AsyncGenerator<File[]>;
@@ -0,0 +1,237 @@
1
+ import * as fsp from 'node:fs/promises';
2
+ import { File, getChildren, getKnownKind } from '../file.js';
3
+ import { Heap } from './heap.js';
4
+ import { ancestorsOf, assertDirectory, byName, createPathError, EntryType, typeOf, Walk } from './walk.js';
5
+ // How the async walk works, and why
6
+ //
7
+ // The walk yields matches in batches, in the same order as the sync walk:
8
+ // depth first, with siblings in name order. Whoever reads the batches sets the
9
+ // pace, so the walk can't run ahead without limit, and stops when they stop.
10
+ //
11
+ // Walking in order on its own is slow, as each directory would wait on its
12
+ // read. So work is started ahead of the walk, within two limits:
13
+ //
14
+ // - WINDOW: within a directory, the siblings after the one being walked are
15
+ // visited early, starting their stats, predicates and reads.
16
+ // - READ_AHEAD: across the tree, directories further on are read early.
17
+ //
18
+ // Directories are read ahead in the order the walk will reach them, using
19
+ // each one's Position and a heap. This order matters, and simpler orders were
20
+ // measured and found slow. Node queues reads, so a read the walk needs now
21
+ // waits behind every read started before it. Reading ahead among siblings only
22
+ // lost the concurrency of reading the tree at once (about 30% slower than
23
+ // reading it all, unbounded). Reading ahead in the order reads finished was
24
+ // worse: the walk's reads waited behind reads for far-off directories, and it
25
+ // was only fast once READ_AHEAD exceeded the number of directories in the tree.
26
+ // In walk order, a READ_AHEAD of 32 is enough to match unbounded reading.
27
+ //
28
+ // So before changing the order of reads, measure it with `npm run bench`, on
29
+ // trees both deep and wide.
30
+ //
31
+ // Work started ahead can fail, or be abandoned, before the walk reaches it.
32
+ // prefetch() stops those failures being reported as unhandled, and an aborted
33
+ // walk starts no new reads.
34
+ // How many siblings are visited ahead of the one being walked. Visiting starts
35
+ // the I/O an entry needs: a directory's read, a link's stat or a predicate's.
36
+ // Each level of the walk has its own window, so the work in flight is bounded
37
+ // by this times the depth.
38
+ const WINDOW = 64;
39
+ // How many more directories can be read ahead of the walk, beyond those in
40
+ // its windows, and not yet walked
41
+ const READ_AHEAD = 64;
42
+ // Work started ahead of the walk may fail before the walk reaches it, and
43
+ // later awaits still see the failure. Until then, Node would otherwise report
44
+ // it as unhandled.
45
+ function prefetch(pending) {
46
+ pending.catch(() => { });
47
+ return pending;
48
+ }
49
+ // Compared index by index, positions give walk order: depth first, in name order
50
+ function precedes(a, b) {
51
+ for (let i = 0; i < a.length && i < b.length; i++) {
52
+ if (a[i] !== b[i]) {
53
+ return a[i] < b[i];
54
+ }
55
+ }
56
+ return a.length < b.length;
57
+ }
58
+ // Reading directories is most of the work of a walk, so they are read ahead
59
+ // of it, in the order the walk will reach them. Reads are queued, so one read
60
+ // out of order would hold up the walk behind the rest.
61
+ class AsyncWalk extends Walk {
62
+ #signal;
63
+ // Read or being read, and not yet taken by the walk
64
+ #reads = new Map();
65
+ // Found but not yet read
66
+ #found = new Heap((a, b) => precedes(a.position, b.position));
67
+ // Of directories, and links that may be, for when the walk reads them
68
+ #positions = new WeakMap();
69
+ #taken = new WeakSet();
70
+ constructor(matcher, opts) {
71
+ super(matcher, opts);
72
+ this.#signal = opts.signal;
73
+ }
74
+ // Rejects as soon as the walk is aborted, rather than when the work is done,
75
+ // which for a slow predicate could be a long wait
76
+ abortable(pending) {
77
+ const signal = this.#signal;
78
+ if (!signal) {
79
+ return pending;
80
+ }
81
+ signal.throwIfAborted();
82
+ return new Promise((resolve, reject) => {
83
+ const abort = () => reject(signal.reason);
84
+ signal.addEventListener('abort', abort, { once: true });
85
+ pending.then((value) => {
86
+ signal.removeEventListener('abort', abort);
87
+ resolve(value);
88
+ }, (error) => {
89
+ signal.removeEventListener('abort', abort);
90
+ reject(error);
91
+ });
92
+ });
93
+ }
94
+ // For after a yield, as whoever reads the walk may have aborted it meanwhile
95
+ throwIfAborted() {
96
+ this.#signal?.throwIfAborted();
97
+ }
98
+ // The directory's children in name order, read ahead if it was reached
99
+ read(dir, depth) {
100
+ this.#taken.add(dir);
101
+ const read = this.#reads.get(dir) ?? this.#start({ dir, depth, position: this.#positions.get(dir) ?? [] });
102
+ this.#reads.delete(dir);
103
+ this.#readAhead();
104
+ return read;
105
+ }
106
+ #start({ dir, depth, position }) {
107
+ return prefetch(getChildren(dir).then((children) => {
108
+ children.sort(byName);
109
+ this.#find(children, depth + 1, position);
110
+ return children;
111
+ }));
112
+ }
113
+ // Links are left to the walk, which needs a stat to know if they're directories
114
+ #find(children, depth, parent) {
115
+ if (!this.canDescend(depth)) {
116
+ return;
117
+ }
118
+ children.forEach((child, i) => {
119
+ const kind = getKnownKind(child);
120
+ if (kind && !kind.isDirectory()) {
121
+ return;
122
+ }
123
+ const position = [...parent, i];
124
+ this.#positions.set(child, position);
125
+ if (kind && !this.shouldSkipDirectory(child)) {
126
+ this.#found.push({ dir: child, depth, position });
127
+ }
128
+ });
129
+ this.#readAhead();
130
+ }
131
+ #readAhead() {
132
+ if (this.#signal?.aborted) {
133
+ return;
134
+ }
135
+ while (this.#reads.size < READ_AHEAD && this.#found.size > 0) {
136
+ const found = this.#found.pop();
137
+ if (!this.#taken.has(found.dir)) {
138
+ this.#reads.set(found.dir, this.#start(found));
139
+ }
140
+ }
141
+ }
142
+ }
143
+ // A linked directory that resolves to one of its own ancestors would be walked forever
144
+ async function isCyclicLink(link) {
145
+ const target = await fsp.realpath(link.getName());
146
+ for (const ancestor of ancestorsOf(link)) {
147
+ if (await fsp.realpath(ancestor) === target) {
148
+ return true;
149
+ }
150
+ }
151
+ return false;
152
+ }
153
+ // Broken links are skipped, as are all links when ignoreSymlinks is set
154
+ async function isVisitableLink(link, walk) {
155
+ if (walk.ignoreSymlinks) {
156
+ return false;
157
+ }
158
+ try {
159
+ return !(await link.isDirectory() && await isCyclicLink(link));
160
+ }
161
+ catch {
162
+ return false;
163
+ }
164
+ }
165
+ const NOTHING = Object.freeze({});
166
+ // Links have no known kind, so need a stat
167
+ async function visitUntyped(file, walk, depth) {
168
+ return visitTyped(file, typeOf(await file.getStats()), walk, depth);
169
+ }
170
+ async function visitLink(link, walk, depth) {
171
+ return await isVisitableLink(link, walk) ? visitUntyped(link, walk, depth) : NOTHING;
172
+ }
173
+ function visitTyped(file, type, walk, depth) {
174
+ if (type === EntryType.DIRECTORY && walk.shouldSkipDirectory(file)) {
175
+ return NOTHING;
176
+ }
177
+ const children = type === EntryType.DIRECTORY && walk.canDescend(depth) ? walk.read(file, depth) : undefined;
178
+ if (!walk.isCandidate(file, type)) {
179
+ return children ? { children } : NOTHING;
180
+ }
181
+ if (walk.isSyncMatcher) {
182
+ return walk.matcher.testSync(file) ? { match: file, children } : children ? { children } : NOTHING;
183
+ }
184
+ return walk.matcher.test(file).then(isMatch => ({ match: isMatch ? file : undefined, children }));
185
+ }
186
+ // Children always have a dirent, so only links lack a known kind
187
+ function visitChild(child, walk, depth) {
188
+ const kind = getKnownKind(child);
189
+ const visited = kind ? visitTyped(child, typeOf(kind), walk, depth) : visitLink(child, walk, depth);
190
+ return visited instanceof Promise ? prefetch(visited) : visited;
191
+ }
192
+ // Matches are yielded in batches, in walk order: an entry, then everything
193
+ // below it, then its next sibling, with siblings in name order. A batch is
194
+ // yielded before walking down, so matches arrive as they are found.
195
+ async function* walkChildren(children, walk, depth) {
196
+ const visits = [];
197
+ let started = 0;
198
+ let matches = [];
199
+ for (let next = 0; next < children.length; next++) {
200
+ for (const end = Math.min(next + WINDOW, children.length); started < end; started++) {
201
+ visits[started] = visitChild(children[started], walk, depth);
202
+ }
203
+ const visited = visits[next];
204
+ visits[next] = undefined;
205
+ const visit = visited instanceof Promise ? await walk.abortable(visited) : visited;
206
+ if (visit.match) {
207
+ matches.push(visit.match);
208
+ }
209
+ if (visit.children) {
210
+ if (matches.length > 0) {
211
+ yield matches;
212
+ walk.throwIfAborted();
213
+ matches = [];
214
+ }
215
+ yield* walkChildren(await walk.abortable(visit.children), walk, depth + 1);
216
+ }
217
+ }
218
+ if (matches.length > 0) {
219
+ yield matches;
220
+ walk.throwIfAborted();
221
+ }
222
+ }
223
+ export async function* walkAsync(dir, matcher, opts) {
224
+ opts.signal?.throwIfAborted();
225
+ const root = new File(dir);
226
+ let stats;
227
+ try {
228
+ stats = await root.getStats();
229
+ }
230
+ catch (error) {
231
+ throw createPathError(dir, error);
232
+ }
233
+ assertDirectory(dir, stats);
234
+ // The root is never a match, only the place to search, so the walk starts at its children
235
+ const walk = new AsyncWalk(matcher, opts);
236
+ yield* walkChildren(await walk.abortable(walk.read(root, 0)), walk, 1);
237
+ }
@@ -0,0 +1,7 @@
1
+ export declare class Heap<T> {
2
+ #private;
3
+ constructor(precedes: (a: T, b: T) => boolean);
4
+ get size(): number;
5
+ push(item: T): void;
6
+ pop(): T | undefined;
7
+ }