tiny-readdir 2.1.0 → 2.2.1

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/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  /* IMPORT */
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { isFunction, makeCounterPromise } from './utils.js';
4
5
  /* MAIN */
5
6
  const readdir = (rootPath, options) => {
6
7
  const followSymlinks = options?.followSymlinks ?? false;
7
8
  const maxDepth = options?.depth ?? Infinity;
8
9
  const maxPaths = options?.limit ?? Infinity;
9
- const isIgnored = options?.ignore ?? (() => false);
10
+ const ignore = options?.ignore ?? (() => false);
11
+ const isIgnored = isFunction(ignore) ? ignore : (targetPath) => ignore.test(targetPath);
10
12
  const signal = options?.signal ?? { aborted: false };
11
13
  const directories = [];
12
14
  const files = [];
@@ -15,6 +17,7 @@ const readdir = (rootPath, options) => {
15
17
  const visited = new Set();
16
18
  const resultEmpty = { directories: [], files: [], symlinks: [], map: {} };
17
19
  const result = { directories, files, symlinks, map };
20
+ const { promise, increment, decrement } = makeCounterPromise();
18
21
  let foundPaths = 0;
19
22
  const handleDirectory = (dirmap, subPath, depth) => {
20
23
  if (visited.has(subPath))
@@ -29,7 +32,7 @@ const readdir = (rootPath, options) => {
29
32
  return;
30
33
  if (foundPaths >= maxPaths)
31
34
  return;
32
- return populateResultFromPath(subPath, depth + 1);
35
+ populateResultFromPath(subPath, depth + 1);
33
36
  };
34
37
  const handleFile = (dirmap, subPath) => {
35
38
  if (visited.has(subPath))
@@ -56,7 +59,7 @@ const readdir = (rootPath, options) => {
56
59
  return;
57
60
  if (foundPaths >= maxPaths)
58
61
  return;
59
- return populateResultFromSymlink(subPath, depth + 1);
62
+ populateResultFromSymlink(subPath, depth + 1);
60
63
  };
61
64
  const handleStat = (dirmap, rootPath, stat, depth) => {
62
65
  if (signal.aborted)
@@ -64,13 +67,13 @@ const readdir = (rootPath, options) => {
64
67
  if (isIgnored(rootPath))
65
68
  return;
66
69
  if (stat.isDirectory()) {
67
- return handleDirectory(dirmap, rootPath, depth);
70
+ handleDirectory(dirmap, rootPath, depth);
68
71
  }
69
72
  else if (stat.isFile()) {
70
- return handleFile(dirmap, rootPath);
73
+ handleFile(dirmap, rootPath);
71
74
  }
72
75
  else if (stat.isSymbolicLink()) {
73
- return handleSymlink(dirmap, rootPath, depth);
76
+ handleSymlink(dirmap, rootPath, depth);
74
77
  }
75
78
  };
76
79
  const handleDirent = (dirmap, rootPath, dirent, depth) => {
@@ -81,48 +84,63 @@ const readdir = (rootPath, options) => {
81
84
  if (isIgnored(subPath))
82
85
  return;
83
86
  if (dirent.isDirectory()) {
84
- return handleDirectory(dirmap, subPath, depth);
87
+ handleDirectory(dirmap, subPath, depth);
85
88
  }
86
89
  else if (dirent.isFile()) {
87
- return handleFile(dirmap, subPath);
90
+ handleFile(dirmap, subPath);
88
91
  }
89
92
  else if (dirent.isSymbolicLink()) {
90
- return handleSymlink(dirmap, subPath, depth);
93
+ handleSymlink(dirmap, subPath, depth);
91
94
  }
92
95
  };
93
96
  const handleDirents = (dirmap, rootPath, dirents, depth) => {
94
- return Promise.all(dirents.map((dirent) => {
95
- return handleDirent(dirmap, rootPath, dirent, depth);
96
- }));
97
+ for (let i = 0, l = dirents.length; i < l; i++) {
98
+ handleDirent(dirmap, rootPath, dirents[i], depth);
99
+ }
97
100
  };
98
- const populateResultFromPath = async (rootPath, depth) => {
101
+ const populateResultFromPath = (rootPath, depth) => {
99
102
  if (signal.aborted)
100
103
  return;
101
104
  if (depth > maxDepth)
102
105
  return;
103
106
  if (foundPaths >= maxPaths)
104
107
  return;
105
- const dirents = await fs.promises.readdir(rootPath, { withFileTypes: true }).catch(() => []);
106
- if (signal.aborted)
107
- return;
108
- const dirmap = map[rootPath] = { directories: [], files: [], symlinks: [] };
109
- if (!dirents.length)
110
- return;
111
- await handleDirents(dirmap, rootPath, dirents, depth);
108
+ increment();
109
+ fs.readdir(rootPath, { withFileTypes: true }, (error, dirents) => {
110
+ if (error)
111
+ return decrement();
112
+ if (signal.aborted)
113
+ return decrement();
114
+ if (!dirents.length)
115
+ return decrement();
116
+ const dirmap = map[rootPath] = { directories: [], files: [], symlinks: [] };
117
+ handleDirents(dirmap, rootPath, dirents, depth);
118
+ decrement();
119
+ });
112
120
  };
113
121
  const populateResultFromSymlink = async (rootPath, depth) => {
114
- try {
115
- const realPath = await fs.promises.realpath(rootPath);
116
- const stat = await fs.promises.stat(realPath);
117
- const dirmap = map[rootPath] = { directories: [], files: [], symlinks: [] };
118
- await handleStat(dirmap, realPath, stat, depth);
119
- }
120
- catch { }
122
+ increment();
123
+ fs.realpath(rootPath, (error, realPath) => {
124
+ if (error)
125
+ return decrement();
126
+ if (signal.aborted)
127
+ return decrement();
128
+ fs.stat(realPath, async (error, stat) => {
129
+ if (error)
130
+ return decrement();
131
+ if (signal.aborted)
132
+ return decrement();
133
+ const dirmap = map[rootPath] = { directories: [], files: [], symlinks: [] };
134
+ handleStat(dirmap, realPath, stat, depth);
135
+ decrement();
136
+ });
137
+ });
121
138
  };
122
139
  const getResult = async (rootPath, depth = 1) => {
123
140
  rootPath = path.normalize(rootPath);
124
141
  visited.add(rootPath);
125
- await populateResultFromPath(rootPath, depth);
142
+ populateResultFromPath(rootPath, depth);
143
+ await promise;
126
144
  if (signal.aborted)
127
145
  return resultEmpty;
128
146
  return result;
package/dist/types.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- type Promisable<T> = Promise<T> | T;
1
+ type Callback = () => void;
2
2
  type Options = {
3
3
  depth?: number;
4
4
  limit?: number;
5
5
  followSymlinks?: boolean;
6
- ignore?: (targetPath: string) => boolean;
6
+ ignore?: ((targetPath: string) => boolean) | RegExp;
7
7
  signal?: {
8
8
  aborted: boolean;
9
9
  };
@@ -19,4 +19,4 @@ type ResultDirectories = {
19
19
  type Result = ResultDirectory & {
20
20
  map: ResultDirectories;
21
21
  };
22
- export type { Promisable, Options, ResultDirectory, ResultDirectories, Result };
22
+ export type { Callback, Options, ResultDirectory, ResultDirectories, Result };
@@ -0,0 +1,8 @@
1
+ import type { Callback } from './types';
2
+ declare const isFunction: (value: unknown) => value is Function;
3
+ declare const makeCounterPromise: () => {
4
+ promise: Promise<void>;
5
+ increment: Callback;
6
+ decrement: Callback;
7
+ };
8
+ export { isFunction, makeCounterPromise };
package/dist/utils.js ADDED
@@ -0,0 +1,22 @@
1
+ /* IMPORT */
2
+ import makeNakedPromise from 'promise-make-naked';
3
+ /* MAIN */
4
+ const isFunction = (value) => {
5
+ return (typeof value === 'function');
6
+ };
7
+ const makeCounterPromise = () => {
8
+ const { promise, resolve } = makeNakedPromise();
9
+ let counter = 0;
10
+ const increment = () => {
11
+ counter += 1;
12
+ };
13
+ const decrement = () => {
14
+ counter -= 1;
15
+ if (counter)
16
+ return;
17
+ resolve();
18
+ };
19
+ return { promise, increment, decrement };
20
+ };
21
+ /* EXPORT */
22
+ export { isFunction, makeCounterPromise };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "tiny-readdir",
3
3
  "repository": "github:fabiospampinato/tiny-readdir",
4
4
  "description": "A simple promisified recursive readdir function.",
5
- "version": "2.1.0",
5
+ "version": "2.2.1",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "exports": "./dist/index.js",
@@ -11,10 +11,9 @@
11
11
  "clean": "tsex clean",
12
12
  "compile": "tsex compile",
13
13
  "compile:watch": "tsex compile --watch",
14
- "test": "npm run test:native && npm run test:yielding",
15
- "test:native": "fava '**/native.js'",
16
- "test:yielding": "node test/yielding.js",
17
- "prepublishOnly": "npm run clean && npm run compile && npm run test"
14
+ "test": "tsex test",
15
+ "test:watch": "tsex test --watch",
16
+ "prepublishOnly": "tsex prepare"
18
17
  },
19
18
  "keywords": [
20
19
  "readdir",
@@ -23,10 +22,13 @@
23
22
  "simple",
24
23
  "tiny"
25
24
  ],
25
+ "dependencies": {
26
+ "promise-make-naked": "^2.1.1"
27
+ },
26
28
  "devDependencies": {
27
- "@types/node": "^18.11.9",
28
- "fava": "^0.0.7",
29
- "tsex": "^1.1.3",
30
- "typescript": "^4.9.3"
29
+ "@types/node": "^20.4.8",
30
+ "fava": "^0.2.1",
31
+ "tsex": "^3.0.1",
32
+ "typescript": "^5.1.6"
31
33
  }
32
34
  }
package/src/index.ts CHANGED
@@ -3,7 +3,8 @@
3
3
 
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
- import type {Promisable, Options, ResultDirectory, ResultDirectories, Result} from './types';
6
+ import {isFunction, makeCounterPromise} from './utils';
7
+ import type {Options, ResultDirectory, ResultDirectories, Result} from './types';
7
8
 
8
9
  /* MAIN */
9
10
 
@@ -12,7 +13,8 @@ const readdir = ( rootPath: string, options?: Options ): Promise<Result> => {
12
13
  const followSymlinks = options?.followSymlinks ?? false;
13
14
  const maxDepth = options?.depth ?? Infinity;
14
15
  const maxPaths = options?.limit ?? Infinity;
15
- const isIgnored = options?.ignore ?? (() => false);
16
+ const ignore = options?.ignore ?? (() => false);
17
+ const isIgnored = isFunction ( ignore ) ? ignore : ( targetPath: string ) => ignore.test ( targetPath );
16
18
  const signal = options?.signal ?? { aborted: false };
17
19
  const directories: string[] = [];
18
20
  const files: string[] = [];
@@ -21,10 +23,11 @@ const readdir = ( rootPath: string, options?: Options ): Promise<Result> => {
21
23
  const visited = new Set<string> ();
22
24
  const resultEmpty: Result = { directories: [], files: [], symlinks: [], map: {} };
23
25
  const result: Result = { directories, files, symlinks, map };
26
+ const {promise, increment, decrement} = makeCounterPromise ();
24
27
 
25
28
  let foundPaths = 0;
26
29
 
27
- const handleDirectory = ( dirmap: ResultDirectory, subPath: string, depth: number ): Promisable<void> => {
30
+ const handleDirectory = ( dirmap: ResultDirectory, subPath: string, depth: number ): void => {
28
31
 
29
32
  if ( visited.has ( subPath ) ) return;
30
33
 
@@ -39,7 +42,7 @@ const readdir = ( rootPath: string, options?: Options ): Promise<Result> => {
39
42
 
40
43
  if ( foundPaths >= maxPaths ) return;
41
44
 
42
- return populateResultFromPath ( subPath, depth + 1 );
45
+ populateResultFromPath ( subPath, depth + 1 );
43
46
 
44
47
  };
45
48
 
@@ -56,7 +59,7 @@ const readdir = ( rootPath: string, options?: Options ): Promise<Result> => {
56
59
 
57
60
  };
58
61
 
59
- const handleSymlink = ( dirmap: ResultDirectory, subPath: string, depth: number ): Promisable<void> => {
62
+ const handleSymlink = ( dirmap: ResultDirectory, subPath: string, depth: number ): void => {
60
63
 
61
64
  if ( visited.has ( subPath ) ) return;
62
65
 
@@ -73,11 +76,11 @@ const readdir = ( rootPath: string, options?: Options ): Promise<Result> => {
73
76
 
74
77
  if ( foundPaths >= maxPaths ) return;
75
78
 
76
- return populateResultFromSymlink ( subPath, depth + 1 );
79
+ populateResultFromSymlink ( subPath, depth + 1 );
77
80
 
78
81
  };
79
82
 
80
- const handleStat = ( dirmap: ResultDirectory, rootPath: string, stat: fs.Stats, depth: number ): Promisable<void> => {
83
+ const handleStat = ( dirmap: ResultDirectory, rootPath: string, stat: fs.Stats, depth: number ): void => {
81
84
 
82
85
  if ( signal.aborted ) return;
83
86
 
@@ -85,21 +88,21 @@ const readdir = ( rootPath: string, options?: Options ): Promise<Result> => {
85
88
 
86
89
  if ( stat.isDirectory () ) {
87
90
 
88
- return handleDirectory ( dirmap, rootPath, depth );
91
+ handleDirectory ( dirmap, rootPath, depth );
89
92
 
90
93
  } else if ( stat.isFile () ) {
91
94
 
92
- return handleFile ( dirmap, rootPath );
95
+ handleFile ( dirmap, rootPath );
93
96
 
94
97
  } else if ( stat.isSymbolicLink () ) {
95
98
 
96
- return handleSymlink ( dirmap, rootPath, depth );
99
+ handleSymlink ( dirmap, rootPath, depth );
97
100
 
98
101
  }
99
102
 
100
103
  };
101
104
 
102
- const handleDirent = ( dirmap: ResultDirectory, rootPath: string, dirent: fs.Dirent, depth: number ): Promisable<void> => {
105
+ const handleDirent = ( dirmap: ResultDirectory, rootPath: string, dirent: fs.Dirent, depth: number ): void => {
103
106
 
104
107
  if ( signal.aborted ) return;
105
108
 
@@ -110,31 +113,31 @@ const readdir = ( rootPath: string, options?: Options ): Promise<Result> => {
110
113
 
111
114
  if ( dirent.isDirectory () ) {
112
115
 
113
- return handleDirectory ( dirmap, subPath, depth );
116
+ handleDirectory ( dirmap, subPath, depth );
114
117
 
115
118
  } else if ( dirent.isFile () ) {
116
119
 
117
- return handleFile ( dirmap, subPath );
120
+ handleFile ( dirmap, subPath );
118
121
 
119
122
  } else if ( dirent.isSymbolicLink () ) {
120
123
 
121
- return handleSymlink ( dirmap, subPath, depth );
124
+ handleSymlink ( dirmap, subPath, depth );
122
125
 
123
126
  }
124
127
 
125
128
  };
126
129
 
127
- const handleDirents = ( dirmap: ResultDirectory, rootPath: string, dirents: fs.Dirent[], depth: number ): Promise<void[]> => {
130
+ const handleDirents = ( dirmap: ResultDirectory, rootPath: string, dirents: fs.Dirent[], depth: number ): void => {
128
131
 
129
- return Promise.all ( dirents.map ( ( dirent ): Promisable<void> => {
132
+ for ( let i = 0, l = dirents.length; i < l; i++ ) {
130
133
 
131
- return handleDirent ( dirmap, rootPath, dirent, depth );
134
+ handleDirent ( dirmap, rootPath, dirents[i], depth );
132
135
 
133
- }));
136
+ }
134
137
 
135
138
  };
136
139
 
137
- const populateResultFromPath = async ( rootPath: string, depth: number ): Promise<void> => {
140
+ const populateResultFromPath = ( rootPath: string, depth: number ): void => {
138
141
 
139
142
  if ( signal.aborted ) return;
140
143
 
@@ -142,29 +145,51 @@ const readdir = ( rootPath: string, options?: Options ): Promise<Result> => {
142
145
 
143
146
  if ( foundPaths >= maxPaths ) return;
144
147
 
145
- const dirents = await fs.promises.readdir ( rootPath, { withFileTypes: true } ).catch ( () => [] );
148
+ increment ();
146
149
 
147
- if ( signal.aborted ) return;
150
+ fs.readdir ( rootPath, { withFileTypes: true }, ( error, dirents ) => {
151
+
152
+ if ( error ) return decrement ();
148
153
 
149
- const dirmap = map[rootPath] = { directories: [], files: [], symlinks: [] };
154
+ if ( signal.aborted ) return decrement ();
150
155
 
151
- if ( !dirents.length ) return;
156
+ if ( !dirents.length ) return decrement ();
157
+
158
+ const dirmap = map[rootPath] = { directories: [], files: [], symlinks: [] };
152
159
 
153
- await handleDirents ( dirmap, rootPath, dirents, depth );
160
+ handleDirents ( dirmap, rootPath, dirents, depth );
161
+
162
+ decrement ();
163
+
164
+ });
154
165
 
155
166
  };
156
167
 
157
168
  const populateResultFromSymlink = async ( rootPath: string, depth: number ): Promise<void> => {
158
169
 
159
- try {
170
+ increment ();
160
171
 
161
- const realPath = await fs.promises.realpath ( rootPath );
162
- const stat = await fs.promises.stat ( realPath );
163
- const dirmap = map[rootPath] = { directories: [], files: [], symlinks: [] };
172
+ fs.realpath ( rootPath, ( error, realPath ) => {
173
+
174
+ if ( error ) return decrement ();
164
175
 
165
- await handleStat ( dirmap, realPath, stat, depth );
176
+ if ( signal.aborted ) return decrement ();
166
177
 
167
- } catch {}
178
+ fs.stat ( realPath, async ( error, stat ) => {
179
+
180
+ if ( error ) return decrement ();
181
+
182
+ if ( signal.aborted ) return decrement ();
183
+
184
+ const dirmap = map[rootPath] = { directories: [], files: [], symlinks: [] };
185
+
186
+ handleStat ( dirmap, realPath, stat, depth );
187
+
188
+ decrement ();
189
+
190
+ });
191
+
192
+ });
168
193
 
169
194
  };
170
195
 
@@ -174,7 +199,9 @@ const readdir = ( rootPath: string, options?: Options ): Promise<Result> => {
174
199
 
175
200
  visited.add ( rootPath );
176
201
 
177
- await populateResultFromPath ( rootPath, depth );
202
+ populateResultFromPath ( rootPath, depth );
203
+
204
+ await promise;
178
205
 
179
206
  if ( signal.aborted ) return resultEmpty;
180
207
 
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  /* HELPERS */
3
3
 
4
- type Promisable<T> = Promise<T> | T;
4
+ type Callback = () => void;
5
5
 
6
6
  /* MAIN */
7
7
 
@@ -9,7 +9,7 @@ type Options = {
9
9
  depth?: number,
10
10
  limit?: number,
11
11
  followSymlinks?: boolean,
12
- ignore?: ( targetPath: string ) => boolean,
12
+ ignore?: (( targetPath: string ) => boolean) | RegExp,
13
13
  signal?: { aborted: boolean }
14
14
  };
15
15
 
@@ -29,4 +29,4 @@ type Result = ResultDirectory & {
29
29
 
30
30
  /* EXPORT */
31
31
 
32
- export type {Promisable, Options, ResultDirectory, ResultDirectories, Result};
32
+ export type {Callback, Options, ResultDirectory, ResultDirectories, Result};
package/src/utils.ts ADDED
@@ -0,0 +1,43 @@
1
+
2
+ /* IMPORT */
3
+
4
+ import makeNakedPromise from 'promise-make-naked';
5
+ import type {Callback} from './types';
6
+
7
+ /* MAIN */
8
+
9
+ const isFunction = ( value: unknown ): value is Function => {
10
+
11
+ return ( typeof value === 'function' );
12
+
13
+ };
14
+
15
+ const makeCounterPromise = (): { promise: Promise<void>, increment: Callback, decrement: Callback } => {
16
+
17
+ const {promise, resolve} = makeNakedPromise<void> ();
18
+
19
+ let counter = 0;
20
+
21
+ const increment = (): void => {
22
+
23
+ counter += 1;
24
+
25
+ };
26
+
27
+ const decrement = (): void => {
28
+
29
+ counter -= 1;
30
+
31
+ if ( counter ) return;
32
+
33
+ resolve ();
34
+
35
+ };
36
+
37
+ return { promise, increment, decrement };
38
+
39
+ };
40
+
41
+ /* EXPORT */
42
+
43
+ export {isFunction, makeCounterPromise};
@@ -164,4 +164,38 @@ describe ( 'Tiny Readdir', it => {
164
164
 
165
165
  });
166
166
 
167
+ it ( 'does not freeze the main thread', async t => {
168
+
169
+ return new Promise ( resolve => {
170
+
171
+ let count = 0;
172
+ let start = Date.now ();
173
+
174
+ const aborter = new AbortController ();
175
+ const signal = aborter.signal;
176
+
177
+ const intervalId = setInterval ( () => {
178
+ count += 1;
179
+ console.log ( 'tick', count );
180
+ if ( count !== 100 ) return;
181
+ clearInterval ( intervalId );
182
+ const end = Date.now ();
183
+ const elapsed = end - start;
184
+ console.log ( 'elapsed', elapsed );
185
+ console.log ( elapsed );
186
+ if ( elapsed > 1500 ) {
187
+ t.fail ();
188
+ } else {
189
+ t.pass ();
190
+ }
191
+ aborter.abort ();
192
+ resolve ();
193
+ }, 10 );
194
+
195
+ readdir ( '/', { signal } );
196
+
197
+ });
198
+
199
+ });
200
+
167
201
  });
package/test/yielding.js DELETED
@@ -1,35 +0,0 @@
1
-
2
- /* IMPORT */
3
-
4
- import readdir from '../dist/index.js';
5
-
6
- /* MAIN */
7
-
8
- const main = async () => {
9
-
10
- let count = 0;
11
- let start = Date.now ();
12
-
13
- setInterval ( () => {
14
- count += 1;
15
- console.log ( 'tick', count );
16
- if ( count < 100 ) return;
17
- const end = Date.now ();
18
- const elapsed = end - start;
19
- console.log ( 'elapsed', elapsed );
20
- if ( elapsed > 1500 ) {
21
- process.exit ( 1 ); // Fail
22
- } else {
23
- process.exit ( 0 ); // Success
24
- }
25
- }, 10 );
26
-
27
- await readdir ( '/' );
28
-
29
- process.exit ( 1 ); // Fail
30
-
31
- };
32
-
33
- /* RUNNING */
34
-
35
- await main ();