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.
- package/README.md +39 -11
- package/dist/arrays.d.ts +1 -0
- package/dist/arrays.js +4 -0
- package/dist/file.d.ts +108 -0
- package/dist/file.js +184 -0
- package/dist/filehound.d.ts +356 -0
- package/dist/filehound.js +498 -0
- package/dist/files.d.ts +4 -0
- package/dist/files.js +37 -0
- package/dist/filetype.d.ts +10 -0
- package/dist/filetype.js +9 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/matchers.d.ts +324 -0
- package/dist/matchers.js +419 -0
- package/dist/walker/async.d.ts +4 -0
- package/dist/walker/async.js +237 -0
- package/dist/walker/heap.d.ts +7 -0
- package/dist/walker/heap.js +49 -0
- package/dist/walker/sync.d.ts +4 -0
- package/dist/walker/sync.js +66 -0
- package/dist/walker/walk.d.ts +33 -0
- package/dist/walker/walk.js +80 -0
- package/package.json +45 -48
- package/.github/stale.yml +0 -42
- package/.travis.yml +0 -8
- package/.vscode/launch.json +0 -21
- package/.vscode/settings.json +0 -3
- package/CONTRIBUTING.md +0 -8
- package/_config.yml +0 -1
- package/coverage/coverage.raw.json +0 -1
- package/coverage/lcov-report/arrays.ts.html +0 -98
- package/coverage/lcov-report/base.css +0 -212
- package/coverage/lcov-report/filehound.ts.html +0 -2078
- package/coverage/lcov-report/files.ts.html +0 -260
- package/coverage/lcov-report/functions.ts.html +0 -122
- package/coverage/lcov-report/index.html +0 -132
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -1
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -158
- package/coverage/lcov.info +0 -378
- package/dist/src/arrays.js +0 -15
- package/dist/src/arrays.js.map +0 -1
- package/dist/src/asyncWalker.js +0 -59
- package/dist/src/asyncWalker.js.map +0 -1
- package/dist/src/bind.js +0 -15
- package/dist/src/bind.js.map +0 -1
- package/dist/src/filehound.js +0 -597
- package/dist/src/filehound.js.map +0 -1
- package/dist/src/files.js +0 -60
- package/dist/src/files.js.map +0 -1
- package/dist/src/matcher.js +0 -51
- package/dist/src/matcher.js.map +0 -1
- package/dist/src/syncWalker.js +0 -47
- package/dist/src/syncWalker.js.map +0 -1
- package/dist/src/walkAsync.js +0 -33
- package/dist/src/walkAsync.js.map +0 -1
- package/dist/src/walkSync.js +0 -21
- package/dist/src/walkSync.js.map +0 -1
- package/dist/src/walker/walk.js +0 -68
- package/dist/src/walker/walk.js.map +0 -1
- package/dist/src/walker/walkAsync.js +0 -33
- package/dist/src/walker/walkAsync.js.map +0 -1
- package/dist/src/walker/walkSync.js +0 -21
- package/dist/src/walker/walkSync.js.map +0 -1
- package/dist/test/filehound.js +0 -872
- package/dist/test/filehound.js.map +0 -1
- package/dist/test/files.js +0 -29
- package/dist/test/files.js.map +0 -1
- package/docconfig/jsdoc.json +0 -26
- package/docconfig/template/LICENSE +0 -61
- package/docconfig/template/README.md +0 -86
- package/docconfig/template/package.json +0 -22
- package/docconfig/template/publish.js +0 -676
- package/docconfig/template/static/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/docconfig/template/static/fonts/OpenSans-Bold-webfont.svg +0 -1830
- package/docconfig/template/static/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/docconfig/template/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/docconfig/template/static/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
- package/docconfig/template/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/docconfig/template/static/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/docconfig/template/static/fonts/OpenSans-Italic-webfont.svg +0 -1830
- package/docconfig/template/static/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/docconfig/template/static/fonts/OpenSans-Light-webfont.eot +0 -0
- package/docconfig/template/static/fonts/OpenSans-Light-webfont.svg +0 -1831
- package/docconfig/template/static/fonts/OpenSans-Light-webfont.woff +0 -0
- package/docconfig/template/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/docconfig/template/static/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
- package/docconfig/template/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/docconfig/template/static/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/docconfig/template/static/fonts/OpenSans-Regular-webfont.svg +0 -1831
- package/docconfig/template/static/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/docconfig/template/static/fonts/OpenSans-Semibold-webfont.eot +0 -0
- package/docconfig/template/static/fonts/OpenSans-Semibold-webfont.svg +0 -1830
- package/docconfig/template/static/fonts/OpenSans-Semibold-webfont.ttf +0 -0
- package/docconfig/template/static/fonts/OpenSans-Semibold-webfont.woff +0 -0
- package/docconfig/template/static/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
- package/docconfig/template/static/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
- package/docconfig/template/static/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
- package/docconfig/template/static/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
- package/docconfig/template/static/scripts/linenumber.js +0 -25
- package/docconfig/template/static/scripts/prettify/Apache-License-2.0.txt +0 -202
- package/docconfig/template/static/scripts/prettify/lang-css.js +0 -2
- package/docconfig/template/static/scripts/prettify/prettify.js +0 -28
- package/docconfig/template/static/styles/jsdoc-default.css +0 -605
- package/docconfig/template/static/styles/prettify-jsdoc.css +0 -111
- package/docconfig/template/static/styles/prettify-tomorrow.css +0 -132
- package/docconfig/template/tmpl/augments.tmpl +0 -10
- package/docconfig/template/tmpl/container.tmpl +0 -183
- package/docconfig/template/tmpl/details.tmpl +0 -143
- package/docconfig/template/tmpl/example.tmpl +0 -2
- package/docconfig/template/tmpl/examples.tmpl +0 -13
- package/docconfig/template/tmpl/exceptions.tmpl +0 -32
- package/docconfig/template/tmpl/layout.tmpl +0 -47
- package/docconfig/template/tmpl/mainpage.tmpl +0 -10
- package/docconfig/template/tmpl/members.tmpl +0 -38
- package/docconfig/template/tmpl/method.tmpl +0 -105
- package/docconfig/template/tmpl/params.tmpl +0 -124
- package/docconfig/template/tmpl/properties.tmpl +0 -108
- package/docconfig/template/tmpl/returns.tmpl +0 -19
- package/docconfig/template/tmpl/source.tmpl +0 -8
- package/docconfig/template/tmpl/tutorial.tmpl +0 -19
- package/docconfig/template/tmpl/type.tmpl +0 -7
- package/docs/FileHound.html +0 -3079
- package/docs/FileHound_modified.html +0 -245
- package/docs/_config.yml +0 -1
- package/docs/filehound.js.html +0 -697
- package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Bold-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Light-webfont.svg +0 -1831
- package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
- package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.svg +0 -1831
- package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Semibold-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Semibold-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Semibold-webfont.ttf +0 -0
- package/docs/fonts/OpenSans-Semibold-webfont.woff +0 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
- package/docs/global.html +0 -288
- package/docs/index.html +0 -154
- package/docs/scripts/linenumber.js +0 -25
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
- package/docs/scripts/prettify/lang-css.js +0 -2
- package/docs/scripts/prettify/prettify.js +0 -28
- package/docs/styles/jsdoc-default.css +0 -605
- package/docs/styles/prettify-jsdoc.css +0 -111
- package/docs/styles/prettify-tomorrow.css +0 -132
- package/docs.md +0 -164
- package/index.js +0 -1
- package/issue_template.md +0 -6
- package/test/filehound.ts +0 -1076
- package/test/files.ts +0 -29
- package/test/fixtures/custom/failed.txt +0 -0
- package/test/fixtures/custom/passed.txt +0 -0
- package/test/fixtures/deeplyNested/c.json +0 -0
- package/test/fixtures/deeplyNested/d.json +0 -0
- package/test/fixtures/deeplyNested/mydir/e.json +0 -0
- package/test/fixtures/deeplyNested/mydir/mydir2/f.json +0 -0
- package/test/fixtures/deeplyNested/mydir/mydir2/mydir3/mydir4/failed.json +0 -0
- package/test/fixtures/deeplyNested/mydir/mydir2/mydir3/z.json +0 -0
- package/test/fixtures/deeplyNested/mydir/mydir2/y.json +0 -0
- package/test/fixtures/deeplyNestedWithHiddenDir/c.json +0 -0
- package/test/fixtures/deeplyNestedWithHiddenDir/d.json +0 -0
- package/test/fixtures/deeplyNestedWithHiddenDir/mydir/e.json +0 -0
- package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/f.json +0 -0
- package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/mydir3/mydir4/.mydir5/.keep +0 -0
- package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/mydir3/mydir4/failed.json +0 -0
- package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/mydir3/z.json +0 -0
- package/test/fixtures/deeplyNestedWithHiddenDir/mydir/mydir2/y.json +0 -0
- package/test/fixtures/ext/Dummy.java +0 -0
- package/test/fixtures/ext/Dumpy.go +0 -0
- package/test/fixtures/ext/dummy.json +0 -0
- package/test/fixtures/ext/dummy.txt +0 -0
- package/test/fixtures/justFiles/a.json +0 -0
- package/test/fixtures/justFiles/b.json +0 -0
- package/test/fixtures/justFiles/dummy.txt +0 -0
- package/test/fixtures/mixed/a.json +0 -0
- package/test/fixtures/mixed/aabbcc.json +0 -0
- package/test/fixtures/mixed/ab.json +0 -0
- package/test/fixtures/mixed/z.json +0 -0
- package/test/fixtures/nested/.hidden1/bad.txt +0 -0
- package/test/fixtures/nested/c.json +0 -0
- package/test/fixtures/nested/d.json +0 -0
- package/test/fixtures/nested/mydir/e.json +0 -0
- package/test/fixtures/sizes/10b.txt +0 -0
- package/test/fixtures/sizes/1b.txt +0 -0
- package/test/fixtures/sizes/1k.txt +0 -0
- package/test/fixtures/sizes/2k.txt +0 -0
- package/test/fixtures/visibility/.hidden/.hidden.json +0 -0
- package/test/fixtures/visibility/.hidden/visible.json +0 -3
- package/test/fixtures/visibility/.hidden.json +0 -3
- package/test/fixtures/visibility/visible/.hidden.json +0 -3
- package/test/fixtures/visibility/visible.json +0 -3
- package/test/mocha.opts +0 -4
- package/tsconfig.json +0 -16
- package/tslint.json +0 -44
package/dist/matchers.js
ADDED
|
@@ -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,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
|
+
}
|