filehound 2.0.0-beta.2 → 2.0.0-beta.3

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 +37 -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 +322 -0
  7. package/dist/filehound.js +443 -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 +4 -0
  13. package/dist/index.js +3 -0
  14. package/dist/matchers.d.ts +297 -0
  15. package/dist/matchers.js +389 -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 +32 -0
  23. package/dist/walker/walk.js +70 -0
  24. package/package.json +44 -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,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
+ }
@@ -0,0 +1,49 @@
1
+ // A binary min-heap: pop returns the item that comes first by precedes
2
+ export class Heap {
3
+ #items = [];
4
+ #precedes;
5
+ constructor(precedes) {
6
+ this.#precedes = precedes;
7
+ }
8
+ get size() {
9
+ return this.#items.length;
10
+ }
11
+ push(item) {
12
+ const items = this.#items;
13
+ let i = items.push(item) - 1;
14
+ while (i > 0) {
15
+ const parent = (i - 1) >> 1;
16
+ if (!this.#precedes(items[i], items[parent])) {
17
+ break;
18
+ }
19
+ [items[i], items[parent]] = [items[parent], items[i]];
20
+ i = parent;
21
+ }
22
+ }
23
+ pop() {
24
+ const items = this.#items;
25
+ const first = items[0];
26
+ const last = items.pop();
27
+ if (items.length > 0 && last !== undefined) {
28
+ items[0] = last;
29
+ let i = 0;
30
+ for (;;) {
31
+ const left = 2 * i + 1;
32
+ const right = left + 1;
33
+ let next = i;
34
+ if (left < items.length && this.#precedes(items[left], items[next])) {
35
+ next = left;
36
+ }
37
+ if (right < items.length && this.#precedes(items[right], items[next])) {
38
+ next = right;
39
+ }
40
+ if (next === i) {
41
+ break;
42
+ }
43
+ [items[i], items[next]] = [items[next], items[i]];
44
+ i = next;
45
+ }
46
+ }
47
+ return first;
48
+ }
49
+ }
@@ -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 walkSync(dir: string, matcher: FilePredicate, opts: WalkOptions): File[];
@@ -0,0 +1,66 @@
1
+ import * as fs from 'node:fs';
2
+ import { File, getChildrenSync, getKnownKind } from '../file.js';
3
+ import { ancestorsOf, assertDirectory, byName, createPathError, EntryType, typeOf, Walk } from './walk.js';
4
+ // A linked directory that resolves to one of its own ancestors would be walked forever
5
+ function isCyclicLink(link) {
6
+ const target = fs.realpathSync(link.getName());
7
+ return ancestorsOf(link).some(ancestor => fs.realpathSync(ancestor) === target);
8
+ }
9
+ // Broken links are skipped, as are all links when ignoreSymlinks is set
10
+ function isVisitableLink(link, walk) {
11
+ if (walk.ignoreSymlinks) {
12
+ return false;
13
+ }
14
+ try {
15
+ return !(link.isDirectorySync() && isCyclicLink(link));
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ // Links have no known kind, so need a stat
22
+ function visitUntyped(file, walk, depth, matches) {
23
+ return visitTyped(file, typeOf(file.getStatsSync()), walk, depth, matches);
24
+ }
25
+ function visitLink(link, walk, depth, matches) {
26
+ return isVisitableLink(link, walk) ? visitUntyped(link, walk, depth, matches) : matches;
27
+ }
28
+ // Entries are visited depth first in name order, adding to a single list of matches
29
+ function visitTyped(file, type, walk, depth, matches) {
30
+ if (type === EntryType.DIRECTORY && walk.shouldSkipDirectory(file)) {
31
+ return matches;
32
+ }
33
+ if (walk.isCandidate(file, type) && walk.matcher.testSync(file)) {
34
+ matches.push(file);
35
+ }
36
+ if (type === EntryType.DIRECTORY && walk.canDescend(depth)) {
37
+ visitChildren(file, walk, depth + 1, matches);
38
+ }
39
+ return matches;
40
+ }
41
+ // Children always have a dirent, so only links lack a known kind
42
+ function visitChildren(dir, walk, depth, matches) {
43
+ for (const child of getChildrenSync(dir).sort(byName)) {
44
+ const kind = getKnownKind(child);
45
+ if (kind) {
46
+ visitTyped(child, typeOf(kind), walk, depth, matches);
47
+ }
48
+ else {
49
+ visitLink(child, walk, depth, matches);
50
+ }
51
+ }
52
+ return matches;
53
+ }
54
+ export function walkSync(dir, matcher, opts) {
55
+ const root = new File(dir);
56
+ let stats;
57
+ try {
58
+ stats = root.getStatsSync();
59
+ }
60
+ catch (error) {
61
+ throw createPathError(dir, error);
62
+ }
63
+ assertDirectory(dir, stats);
64
+ // The root is never a match, only the place to search, so the walk starts at its children
65
+ return visitChildren(root, new Walk(matcher, opts), 1, []);
66
+ }
@@ -0,0 +1,32 @@
1
+ import type { File, FileKind } from '../file.js';
2
+ import { FileType } from '../filetype.js';
3
+ import { type FilePredicate } from '../matchers.js';
4
+ export type WalkOptions = {
5
+ maxDepth?: number;
6
+ ignoreHiddenFiles: boolean;
7
+ ignoreHiddenDirectories: boolean;
8
+ ignoreSymlinks: boolean;
9
+ fileTypes: FileType[];
10
+ signal?: AbortSignal;
11
+ };
12
+ export declare const EntryType: {
13
+ readonly OTHER: "other";
14
+ readonly REGULAR: "regular";
15
+ readonly DIRECTORY: "directory";
16
+ };
17
+ export type EntryType = typeof EntryType[keyof typeof EntryType];
18
+ export declare function createPathError(name: string, cause: unknown): Error;
19
+ export declare function assertDirectory(name: string, stats: FileKind): void;
20
+ export declare class Walk {
21
+ #private;
22
+ readonly matcher: FilePredicate;
23
+ readonly isSyncMatcher: boolean;
24
+ constructor(matcher: FilePredicate, opts: WalkOptions);
25
+ get ignoreSymlinks(): boolean;
26
+ isCandidate(file: File, type: EntryType): boolean;
27
+ shouldSkipDirectory(dir: File): boolean;
28
+ canDescend(depth: number): boolean;
29
+ }
30
+ export declare function typeOf(kind: FileKind): EntryType;
31
+ export declare function byName(a: File, b: File): number;
32
+ export declare function ancestorsOf(file: File): string[];
@@ -0,0 +1,70 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { FileType } from '../filetype.js';
3
+ import { isSyncPredicate } from '../matchers.js';
4
+ // Every entry has one. FileType is the public subset; anything else, such as a
5
+ // socket or FIFO, is OTHER and never matched.
6
+ export const EntryType = { ...FileType, OTHER: 'other' };
7
+ export function createPathError(name, cause) {
8
+ return new Error(`'${name}' is not a valid path`, { cause });
9
+ }
10
+ // Search paths are where to search, so must be directories
11
+ export function assertDirectory(name, stats) {
12
+ if (!stats.isDirectory()) {
13
+ throw new Error(`'${name}' is not a directory`);
14
+ }
15
+ }
16
+ // State shared by a walk. The async and sync walks (async.ts and sync.ts) share
17
+ // everything except the calls that touch the file system, and both return
18
+ // matches in name order.
19
+ export class Walk {
20
+ matcher;
21
+ // Checked once, as combined predicates work it out from their parts
22
+ isSyncMatcher;
23
+ #opts;
24
+ constructor(matcher, opts) {
25
+ this.matcher = matcher;
26
+ this.isSyncMatcher = isSyncPredicate(matcher);
27
+ this.#opts = opts;
28
+ }
29
+ get ignoreSymlinks() {
30
+ return this.#opts.ignoreSymlinks;
31
+ }
32
+ isCandidate(file, type) {
33
+ switch (type) {
34
+ case EntryType.DIRECTORY:
35
+ return this.#typeEnabled(type);
36
+ case EntryType.REGULAR:
37
+ return this.#typeEnabled(type) && !(this.#opts.ignoreHiddenFiles && file.isHiddenSync());
38
+ case EntryType.OTHER:
39
+ return false;
40
+ }
41
+ }
42
+ // Hidden directories are neither returned nor walked
43
+ shouldSkipDirectory(dir) {
44
+ return this.#opts.ignoreHiddenDirectories && dir.isHiddenSync();
45
+ }
46
+ // depth counts levels below the root, which is 0. A directory beyond
47
+ // maxDepth is still returned, like any other entry at its level, but not walked.
48
+ canDescend(depth) {
49
+ return this.#opts.maxDepth === undefined || depth <= this.#opts.maxDepth;
50
+ }
51
+ #typeEnabled(type) {
52
+ return this.#opts.fileTypes.includes(type);
53
+ }
54
+ }
55
+ export function typeOf(kind) {
56
+ return kind.isDirectory() ? EntryType.DIRECTORY : kind.isFile() ? EntryType.REGULAR : EntryType.OTHER;
57
+ }
58
+ export function byName(a, b) {
59
+ return a.getName() < b.getName() ? -1 : a.getName() > b.getName() ? 1 : 0;
60
+ }
61
+ export function ancestorsOf(file) {
62
+ const ancestors = [];
63
+ let ancestor = dirname(resolve(file.getName()));
64
+ while (dirname(ancestor) !== ancestor) {
65
+ ancestors.push(ancestor);
66
+ ancestor = dirname(ancestor);
67
+ }
68
+ ancestors.push(ancestor);
69
+ return ancestors;
70
+ }
package/package.json CHANGED
@@ -1,30 +1,44 @@
1
1
  {
2
2
  "name": "filehound",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.3",
4
4
  "description": "Find files the easy way",
5
- "main": "index.js",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=24"
19
+ },
6
20
  "scripts": {
7
- "jsdoc": "jsdoc -c ./docconfig/jsdoc.json",
21
+ "docs": "typedoc",
8
22
  "build": "tsc",
23
+ "typecheck": "tsc -p tsconfig.test.json",
9
24
  "watch": "tsc --watch",
10
- "clean": "rm -fr dist/*",
11
- "prepublish": "npm run build",
12
- "prepublishOnly": "tsc -p ./ --outDir dist",
13
- "test": "mocha -r ts-node/register test/*.ts",
25
+ "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
26
+ "prepublishOnly": "npm run clean && npm run build",
27
+ "test": "mocha",
14
28
  "posttest": "npm run lint",
15
- "lint": "tslint --project tsconfig.json",
16
- "coverage": "nyc mocha && nyc report --reporter=html && nyc report --reporter=json-summary"
29
+ "lint": "eslint src test bench",
30
+ "coverage": "c8 mocha",
31
+ "bench": "npm run build && node bench/bench.mjs"
17
32
  },
18
33
  "repository": {
19
34
  "type": "git",
20
- "url": "https://github.com/nspragg/filehound.git"
35
+ "url": "git+https://github.com/nspragg/filehound.git"
21
36
  },
22
37
  "keywords": [
23
38
  "file",
24
39
  "recursive",
25
40
  "search",
26
41
  "filehound",
27
- "file",
28
42
  "hound",
29
43
  "lister",
30
44
  "find",
@@ -36,48 +50,30 @@
36
50
  "fs",
37
51
  "file system"
38
52
  ],
53
+ "author": "nspragg <nspragg@gmail.com>",
39
54
  "license": "MIT",
40
55
  "bugs": {
41
56
  "url": "https://github.com/nspragg/filehound/issues"
42
57
  },
43
58
  "homepage": "https://github.com/nspragg/filehound",
44
- "devDependencies": {
45
- "@types/mocha": "^2.2.43",
46
- "@types/node": "^8.0.33",
47
- "@types/typescript": "^2.0.0",
48
- "eslint-config-iplayer-base": "^1.0.0",
49
- "chai": "^4.1.2",
50
- "coveralls": "^2.11.9",
51
- "eslint": "^3.11.1",
52
- "istanbul": "1.1.0-alpha.1",
53
- "jsdoc": "^3.4.3",
54
- "mocha": "^5.0.5",
55
- "sinon": "^2.1.0",
56
- "ts-node": "^3.3.0",
57
- "tslint": "^5.8.0",
58
- "tslint-config-airbnb": "^5.3.1",
59
- "tslint-eslint-rules": "^4.1.1",
60
- "typescript": "^2.6.1"
61
- },
62
- "nyc": {
63
- "include": [
64
- "src/**/*.ts"
65
- ],
66
- "extension": [
67
- ".ts"
68
- ],
69
- "require": [
70
- "ts-node/register"
71
- ],
72
- "sourceMap": true,
73
- "instrument": true
74
- },
75
59
  "dependencies": {
76
- "file-js": "0.3.0",
77
- "lodash": "^4.17.5",
78
- "minimatch": "^3.0.4",
79
- "moment": "^2.22.0",
80
- "npm": "^5.8.0",
81
- "unit-compare": "^1.0.1"
60
+ "unit-compare": "^2.0.0"
61
+ },
62
+ "devDependencies": {
63
+ "@eslint/js": "^10.0.1",
64
+ "@stylistic/eslint-plugin": "^5.10.0",
65
+ "@types/chai": "^5.2.3",
66
+ "@types/mocha": "^10.0.10",
67
+ "@types/node": "^24.19.2",
68
+ "@types/sinon": "^22.0.2",
69
+ "c8": "^12.0.0",
70
+ "chai": "^6.3.0",
71
+ "eslint": "^10.13.0",
72
+ "mocha": "^12.0.3",
73
+ "sinon": "^22.1.0",
74
+ "tsx": "^4.23.15",
75
+ "typedoc": "^0.28.20",
76
+ "typescript": "~6.0",
77
+ "typescript-eslint": "^8.72.1"
82
78
  }
83
79
  }
package/.github/stale.yml DELETED
@@ -1,42 +0,0 @@
1
- # Configuration for probot-stale - https://github.com/probot/stale
2
-
3
- # Number of days of inactivity before an Issue or Pull Request becomes stale
4
- daysUntilStale: 5
5
- # Number of days of inactivity before a stale Issue or Pull Request is closed.
6
- # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
7
- daysUntilClose: 7
8
- # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
9
- exemptLabels:
10
- - pinned
11
- - security
12
- - "[Status] Maybe Later"
13
- # Set to true to ignore issues in a project (defaults to false)
14
- exemptProjects: false
15
- # Set to true to ignore issues in a milestone (defaults to false)
16
- exemptMilestones: false
17
- # Label to use when marking as stale
18
- staleLabel: wontfix
19
- # Comment to post when marking as stale. Set to `false` to disable
20
- markComment: >
21
- This issue has been automatically marked as stale because it has not had
22
- recent activity. It will be closed if no further activity occurs. Thank you
23
- for your contributions.
24
- # Comment to post when removing the stale label. Set to `false` to disable
25
- unmarkComment: false
26
- # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable
27
- closeComment: false
28
- # Limit the number of actions per hour, from 1-30. Default is 30
29
- limitPerRun: 30
30
- # Limit to only `issues` or `pulls`
31
- # only: issues
32
- #
33
- # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
34
- # pulls:
35
- # daysUntilStale: 30
36
- # markComment: >
37
- # This pull request has been automatically marked as stale because it has not had
38
- # recent activity. It will be closed if no further activity occurs. Thank you
39
- # for your contributions.
40
- # issues:
41
- # exemptLabels:
42
- # - confirmed
package/.travis.yml DELETED
@@ -1,8 +0,0 @@
1
- language: node_js
2
- os:
3
- - linux
4
- node_js:
5
- - "8"
6
- after_script:
7
- - npm run coverage
8
- - cat coverage/lcov.info | node_modules/.bin/coveralls
@@ -1,21 +0,0 @@
1
- {
2
- // Use IntelliSense to learn about possible Node.js debug attributes.
3
- // Hover to view descriptions of existing attributes.
4
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
- "version": "0.2.0",
6
- "configurations": [
7
- {
8
- "type": "node",
9
- "request": "launch",
10
- "name": "Launch Program",
11
- "program": "${workspaceRoot}/index.js"
12
- },
13
- {
14
- "type": "node",
15
- "request": "attach",
16
- "name": "Attach to Process",
17
- "address": "localhost",
18
- "port": 5858
19
- }
20
- ]
21
- }
@@ -1,3 +0,0 @@
1
- // Place your settings in this file to overwrite default and user settings.
2
- {
3
- }