@statewalker/webrun-files-node 0.9.0 → 0.10.2

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 CHANGED
@@ -32,7 +32,11 @@ console.log(content); // {"debug": true}
32
32
 
33
33
  // List files
34
34
  for await (const entry of files.list('/')) {
35
- console.log(entry.name, entry.kind, entry.size);
35
+ if (entry.kind === 'file') {
36
+ console.log(entry.name, entry.kind, entry.size, entry.lastModified);
37
+ } else {
38
+ console.log(entry.name, entry.kind); // directories carry no size or time
39
+ }
36
40
  }
37
41
  ```
38
42
 
@@ -57,6 +61,9 @@ virtual path: "/users/alice.json"
57
61
  real path: "/var/app/data/users/alice.json"
58
62
  ```
59
63
 
64
+ Paths are appended to `rootDir` without confinement: `..` segments are kept, not resolved, so a
65
+ virtual path such as `/../etc/passwd` reaches outside `rootDir`. Do not pass untrusted paths.
66
+
60
67
  ## API Reference
61
68
 
62
69
  ### NodeFilesApi
@@ -65,7 +72,7 @@ real path: "/var/app/data/users/alice.json"
65
72
  interface NodeFilesApiOptions {
66
73
  /**
67
74
  * Root directory for file operations.
68
- * All paths are resolved relative to this directory.
75
+ * Virtual paths are appended to this directory (see "Path Mapping" for `..`).
69
76
  * Defaults to current working directory if not specified.
70
77
  */
71
78
  rootDir?: string;
@@ -114,9 +121,17 @@ Parent directories are created automatically when writing files:
114
121
  await writeText(files, '/deep/nested/path/file.txt', 'content');
115
122
  ```
116
123
 
124
+ ### Ordered Listings
125
+
126
+ `readdir` returns names in no guaranteed order, so `list()` reads each directory, stats its entries
127
+ and yields them in path order (as every `FilesApi` does). A recursive listing reads a directory only
128
+ when the listing reaches it, and with `{ after }` skips any directory whose whole subtree sorts at
129
+ or before the cursor.
130
+
117
131
  ### Recursive Operations
118
132
 
119
- Copy and remove work recursively on directories:
133
+ Copy and remove work recursively on directories. `move()` is a single `fs.rename`: when the rename
134
+ fails — for example across devices — it returns `false` rather than falling back to copying.
120
135
 
121
136
  ```typescript
122
137
  // Copy entire directory tree
@@ -133,6 +148,7 @@ import { mkdtemp, rm } from 'node:fs/promises';
133
148
  import { tmpdir } from 'node:os';
134
149
  import { join } from 'node:path';
135
150
  import { NodeFilesApi } from '@statewalker/webrun-files-node';
151
+ import { writeText } from '@statewalker/webrun-files';
136
152
 
137
153
  // Create a temp directory for testing
138
154
  const tempDir = await mkdtemp(join(tmpdir(), 'test-'));
@@ -25,6 +25,89 @@ let node_fs_promises = require("node:fs/promises");
25
25
  node_fs_promises = __toESM(node_fs_promises, 1);
26
26
  //#region ../webrun-files/dist/esm/index.js
27
27
  /**
28
+ * The order every `FilesApi.list()` yields: paths compared by Unicode code
29
+ * point, which is the same as comparing their UTF-8 bytes — the order of
30
+ * SQLite's `BINARY` collation and of S3 key listings.
31
+ *
32
+ * Not JavaScript's `<`, which compares UTF-16 code units and so puts a
33
+ * character above U+FFFF (a surrogate pair) before U+E000–U+FFFF.
34
+ */
35
+ function comparePaths(a, b) {
36
+ const length = Math.min(a.length, b.length);
37
+ for (let i = 0; i < length; i++) {
38
+ const x = a.charCodeAt(i);
39
+ const y = b.charCodeAt(i);
40
+ if (x === y) continue;
41
+ const xSurrogate = x >= 55296 && x <= 57343;
42
+ if (xSurrogate !== (y >= 55296 && y <= 57343)) return xSurrogate ? 1 : -1;
43
+ return x - y;
44
+ }
45
+ return a.length - b.length;
46
+ }
47
+ /** Whether a listing resuming after `after` can still yield anything under `dir`. */
48
+ function subtreeMayFollow(dir, after) {
49
+ return after === void 0 || comparePaths(after, `${dir === "/" ? "" : dir}0`) < 0;
50
+ }
51
+ /**
52
+ * List a tree in path order for backends that read one directory at a time.
53
+ * `children(dir)` returns a directory's direct entries in any order.
54
+ *
55
+ * Pending entries sit in a min-heap keyed by path. A directory's children all
56
+ * sort after it, so pushing them when the directory is taken never yields out
57
+ * of order. Children are read only when the listing reaches their directory,
58
+ * and never for a directory whose whole subtree lies at or before `after`.
59
+ */
60
+ async function* listInPathOrder(dir, children, options = {}) {
61
+ const { recursive = false, after } = options;
62
+ const heap = new PathHeap((entry) => entry.path);
63
+ if (!subtreeMayFollow(dir, after)) return;
64
+ for (const entry of await children(dir)) heap.push(entry);
65
+ for (let entry = heap.pop(); entry; entry = heap.pop()) {
66
+ if (after === void 0 || comparePaths(entry.path, after) > 0) yield entry;
67
+ if (recursive && entry.kind === "directory" && subtreeMayFollow(entry.path, after)) for (const child of await children(entry.path)) heap.push(child);
68
+ }
69
+ }
70
+ /** A binary min-heap ordered by {@link comparePaths}. */
71
+ var PathHeap = class {
72
+ #items = [];
73
+ #key;
74
+ constructor(key) {
75
+ this.#key = key;
76
+ }
77
+ push(item) {
78
+ const items = this.#items;
79
+ items.push(item);
80
+ for (let i = items.length - 1; i > 0;) {
81
+ const parent = i - 1 >> 1;
82
+ if (this.#less(items[parent], items[i])) break;
83
+ [items[parent], items[i]] = [items[i], items[parent]];
84
+ i = parent;
85
+ }
86
+ }
87
+ pop() {
88
+ const items = this.#items;
89
+ const top = items[0];
90
+ const last = items.pop();
91
+ if (items.length > 0 && last !== void 0) {
92
+ items[0] = last;
93
+ for (let i = 0;;) {
94
+ const left = 2 * i + 1;
95
+ const right = left + 1;
96
+ let smallest = i;
97
+ if (left < items.length && this.#less(items[left], items[smallest])) smallest = left;
98
+ if (right < items.length && this.#less(items[right], items[smallest])) smallest = right;
99
+ if (smallest === i) break;
100
+ [items[smallest], items[i]] = [items[i], items[smallest]];
101
+ i = smallest;
102
+ }
103
+ }
104
+ return top;
105
+ }
106
+ #less(a, b) {
107
+ return comparePaths(this.#key(a), this.#key(b)) < 0;
108
+ }
109
+ };
110
+ /**
28
111
  * Path manipulation utilities for virtual filesystem paths.
29
112
  * All paths use forward slashes and start with "/".
30
113
  */
@@ -123,36 +206,40 @@ var NodeFilesApi = class {
123
206
  const realPath = this.resolvePath(path);
124
207
  await node_fs_promises.mkdir(realPath, { recursive: true });
125
208
  }
209
+ /** `readdir` order is unspecified, so the tree is walked in `comparePaths` order. */
126
210
  async *list(path, options) {
127
- const realPath = this.resolvePath(path);
128
- const normalizedPath = normalizePath(path);
211
+ yield* listInPathOrder(normalizePath(path), (dir) => this.readChildren(dir), options);
212
+ }
213
+ /** A directory's direct entries, in `readdir` order; none if it cannot be read. */
214
+ async readChildren(dir) {
215
+ let entries;
129
216
  try {
130
- const entries = await node_fs_promises.readdir(realPath, { withFileTypes: true });
131
- for (const entry of entries) {
132
- const entryPath = joinPath(normalizedPath, entry.name);
133
- const fullPath = this.resolvePath(entryPath);
134
- let stat;
135
- try {
136
- stat = await node_fs_promises.stat(fullPath);
137
- } catch {
138
- continue;
139
- }
140
- yield entry.isDirectory() ? {
141
- name: entry.name,
142
- path: entryPath,
143
- kind: "directory"
144
- } : {
145
- name: entry.name,
146
- path: entryPath,
147
- kind: "file",
148
- size: stat.size,
149
- lastModified: stat.mtimeMs
150
- };
151
- if (options?.recursive && entry.isDirectory()) yield* this.list(entryPath, options);
152
- }
217
+ entries = await node_fs_promises.readdir(this.resolvePath(dir), { withFileTypes: true });
153
218
  } catch {
154
- return;
219
+ return [];
220
+ }
221
+ const infos = [];
222
+ for (const entry of entries) {
223
+ const entryPath = joinPath(dir, entry.name);
224
+ let stat;
225
+ try {
226
+ stat = await node_fs_promises.stat(this.resolvePath(entryPath));
227
+ } catch {
228
+ continue;
229
+ }
230
+ infos.push(entry.isDirectory() ? {
231
+ name: entry.name,
232
+ path: entryPath,
233
+ kind: "directory"
234
+ } : {
235
+ name: entry.name,
236
+ path: entryPath,
237
+ kind: "file",
238
+ size: stat.size,
239
+ lastModified: stat.mtimeMs
240
+ });
155
241
  }
242
+ return infos;
156
243
  }
157
244
  async stats(path) {
158
245
  const realPath = this.resolvePath(path);
package/dist/esm/index.js CHANGED
@@ -1,6 +1,89 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  //#region ../webrun-files/dist/esm/index.js
3
3
  /**
4
+ * The order every `FilesApi.list()` yields: paths compared by Unicode code
5
+ * point, which is the same as comparing their UTF-8 bytes — the order of
6
+ * SQLite's `BINARY` collation and of S3 key listings.
7
+ *
8
+ * Not JavaScript's `<`, which compares UTF-16 code units and so puts a
9
+ * character above U+FFFF (a surrogate pair) before U+E000–U+FFFF.
10
+ */
11
+ function comparePaths(a, b) {
12
+ const length = Math.min(a.length, b.length);
13
+ for (let i = 0; i < length; i++) {
14
+ const x = a.charCodeAt(i);
15
+ const y = b.charCodeAt(i);
16
+ if (x === y) continue;
17
+ const xSurrogate = x >= 55296 && x <= 57343;
18
+ if (xSurrogate !== (y >= 55296 && y <= 57343)) return xSurrogate ? 1 : -1;
19
+ return x - y;
20
+ }
21
+ return a.length - b.length;
22
+ }
23
+ /** Whether a listing resuming after `after` can still yield anything under `dir`. */
24
+ function subtreeMayFollow(dir, after) {
25
+ return after === void 0 || comparePaths(after, `${dir === "/" ? "" : dir}0`) < 0;
26
+ }
27
+ /**
28
+ * List a tree in path order for backends that read one directory at a time.
29
+ * `children(dir)` returns a directory's direct entries in any order.
30
+ *
31
+ * Pending entries sit in a min-heap keyed by path. A directory's children all
32
+ * sort after it, so pushing them when the directory is taken never yields out
33
+ * of order. Children are read only when the listing reaches their directory,
34
+ * and never for a directory whose whole subtree lies at or before `after`.
35
+ */
36
+ async function* listInPathOrder(dir, children, options = {}) {
37
+ const { recursive = false, after } = options;
38
+ const heap = new PathHeap((entry) => entry.path);
39
+ if (!subtreeMayFollow(dir, after)) return;
40
+ for (const entry of await children(dir)) heap.push(entry);
41
+ for (let entry = heap.pop(); entry; entry = heap.pop()) {
42
+ if (after === void 0 || comparePaths(entry.path, after) > 0) yield entry;
43
+ if (recursive && entry.kind === "directory" && subtreeMayFollow(entry.path, after)) for (const child of await children(entry.path)) heap.push(child);
44
+ }
45
+ }
46
+ /** A binary min-heap ordered by {@link comparePaths}. */
47
+ var PathHeap = class {
48
+ #items = [];
49
+ #key;
50
+ constructor(key) {
51
+ this.#key = key;
52
+ }
53
+ push(item) {
54
+ const items = this.#items;
55
+ items.push(item);
56
+ for (let i = items.length - 1; i > 0;) {
57
+ const parent = i - 1 >> 1;
58
+ if (this.#less(items[parent], items[i])) break;
59
+ [items[parent], items[i]] = [items[i], items[parent]];
60
+ i = parent;
61
+ }
62
+ }
63
+ pop() {
64
+ const items = this.#items;
65
+ const top = items[0];
66
+ const last = items.pop();
67
+ if (items.length > 0 && last !== void 0) {
68
+ items[0] = last;
69
+ for (let i = 0;;) {
70
+ const left = 2 * i + 1;
71
+ const right = left + 1;
72
+ let smallest = i;
73
+ if (left < items.length && this.#less(items[left], items[smallest])) smallest = left;
74
+ if (right < items.length && this.#less(items[right], items[smallest])) smallest = right;
75
+ if (smallest === i) break;
76
+ [items[smallest], items[i]] = [items[i], items[smallest]];
77
+ i = smallest;
78
+ }
79
+ }
80
+ return top;
81
+ }
82
+ #less(a, b) {
83
+ return comparePaths(this.#key(a), this.#key(b)) < 0;
84
+ }
85
+ };
86
+ /**
4
87
  * Path manipulation utilities for virtual filesystem paths.
5
88
  * All paths use forward slashes and start with "/".
6
89
  */
@@ -99,36 +182,40 @@ var NodeFilesApi = class {
99
182
  const realPath = this.resolvePath(path);
100
183
  await fs.mkdir(realPath, { recursive: true });
101
184
  }
185
+ /** `readdir` order is unspecified, so the tree is walked in `comparePaths` order. */
102
186
  async *list(path, options) {
103
- const realPath = this.resolvePath(path);
104
- const normalizedPath = normalizePath(path);
187
+ yield* listInPathOrder(normalizePath(path), (dir) => this.readChildren(dir), options);
188
+ }
189
+ /** A directory's direct entries, in `readdir` order; none if it cannot be read. */
190
+ async readChildren(dir) {
191
+ let entries;
105
192
  try {
106
- const entries = await fs.readdir(realPath, { withFileTypes: true });
107
- for (const entry of entries) {
108
- const entryPath = joinPath(normalizedPath, entry.name);
109
- const fullPath = this.resolvePath(entryPath);
110
- let stat;
111
- try {
112
- stat = await fs.stat(fullPath);
113
- } catch {
114
- continue;
115
- }
116
- yield entry.isDirectory() ? {
117
- name: entry.name,
118
- path: entryPath,
119
- kind: "directory"
120
- } : {
121
- name: entry.name,
122
- path: entryPath,
123
- kind: "file",
124
- size: stat.size,
125
- lastModified: stat.mtimeMs
126
- };
127
- if (options?.recursive && entry.isDirectory()) yield* this.list(entryPath, options);
128
- }
193
+ entries = await fs.readdir(this.resolvePath(dir), { withFileTypes: true });
129
194
  } catch {
130
- return;
195
+ return [];
196
+ }
197
+ const infos = [];
198
+ for (const entry of entries) {
199
+ const entryPath = joinPath(dir, entry.name);
200
+ let stat;
201
+ try {
202
+ stat = await fs.stat(this.resolvePath(entryPath));
203
+ } catch {
204
+ continue;
205
+ }
206
+ infos.push(entry.isDirectory() ? {
207
+ name: entry.name,
208
+ path: entryPath,
209
+ kind: "directory"
210
+ } : {
211
+ name: entry.name,
212
+ path: entryPath,
213
+ kind: "file",
214
+ size: stat.size,
215
+ lastModified: stat.mtimeMs
216
+ });
131
217
  }
218
+ return infos;
132
219
  }
133
220
  async stats(path) {
134
221
  const realPath = this.resolvePath(path);
@@ -17,7 +17,10 @@ export declare class NodeFilesApi implements FilesApi {
17
17
  read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
18
18
  write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
19
19
  mkdir(path: string): Promise<void>;
20
+ /** `readdir` order is unspecified, so the tree is walked in `comparePaths` order. */
20
21
  list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
22
+ /** A directory's direct entries, in `readdir` order; none if it cannot be read. */
23
+ private readChildren;
21
24
  stats(path: string): Promise<FileStats | undefined>;
22
25
  exists(path: string): Promise<boolean>;
23
26
  remove(path: string): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"node-files-api.d.ts","sourceRoot":"","sources":["../src/node-files-api.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,YAAa,YAAW,QAAQ;IAC3C,OAAO,CAAC,OAAO,CAAS;IAExB,YAAY,OAAO,CAAC,EAAE,mBAAmB,EAExC;IAED,OAAO,CAAC,WAAW;IAKZ,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC,CAyC1E;IAEK,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAuBf;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGvC;IAEM,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC,CAsCxE;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAkBxD;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS3C;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAc3C;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAmB3D;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAmB3D;CACF"}
1
+ {"version":3,"file":"node-files-api.d.ts","sourceRoot":"","sources":["../src/node-files-api.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,YAAa,YAAW,QAAQ;IAC3C,OAAO,CAAC,OAAO,CAAS;IAExB,YAAY,OAAO,CAAC,EAAE,mBAAmB,EAExC;IAED,OAAO,CAAC,WAAW;IAKZ,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC,CAyC1E;IAEK,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAuBf;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGvC;IAED,qFAAqF;IAC9E,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC,CAExE;IAED,mFAAmF;YACrE,YAAY;IAgCpB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAkBxD;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS3C;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAc3C;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAmB3D;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAmB3D;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-files-node",
3
- "version": "0.9.0",
3
+ "version": "0.10.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Node.js FilesApi implementation",
@@ -19,6 +19,7 @@
19
19
  "types": "./dist/index.d.ts",
20
20
  "exports": {
21
21
  ".": {
22
+ "source": "./src/index.ts",
22
23
  "types": "./dist/index.d.ts",
23
24
  "import": "./dist/esm/index.js",
24
25
  "require": "./dist/cjs/index.cjs"
@@ -29,15 +30,15 @@
29
30
  "src"
30
31
  ],
31
32
  "dependencies": {
32
- "@statewalker/webrun-files": "0.9.0"
33
+ "@statewalker/webrun-files": "^0.10.1"
33
34
  },
34
35
  "devDependencies": {
35
- "@types/node": "^26.2.0",
36
+ "@types/node": "^26.6.4",
36
37
  "rimraf": "^6.1.3",
37
- "rolldown": "^1.2.4",
38
+ "rolldown": "^1.2.12",
38
39
  "typescript": "^7.0.2",
39
- "vitest": "^4.1.10",
40
- "@statewalker/webrun-files-tests": "2.0.0"
40
+ "vitest": "^5.0.3",
41
+ "@statewalker/webrun-files-tests": "^3.0.0"
41
42
  },
42
43
  "sideEffects": false,
43
44
  "publishConfig": {
@@ -1,4 +1,4 @@
1
- import type { Stats } from "node:fs";
1
+ import type { Dirent, Stats } from "node:fs";
2
2
  import * as fs from "node:fs/promises";
3
3
  import type {
4
4
  FileInfo,
@@ -7,7 +7,7 @@ import type {
7
7
  ListOptions,
8
8
  ReadOptions,
9
9
  } from "@statewalker/webrun-files";
10
- import { dirname, joinPath, normalizePath } from "@statewalker/webrun-files";
10
+ import { dirname, joinPath, listInPathOrder, normalizePath } from "@statewalker/webrun-files";
11
11
 
12
12
  export interface NodeFilesApiOptions {
13
13
  /**
@@ -109,25 +109,31 @@ export class NodeFilesApi implements FilesApi {
109
109
  await fs.mkdir(realPath, { recursive: true });
110
110
  }
111
111
 
112
+ /** `readdir` order is unspecified, so the tree is walked in `comparePaths` order. */
112
113
  async *list(path: string, options?: ListOptions): AsyncIterable<FileInfo> {
113
- const realPath = this.resolvePath(path);
114
- const normalizedPath = normalizePath(path);
114
+ yield* listInPathOrder(normalizePath(path), (dir) => this.readChildren(dir), options);
115
+ }
115
116
 
117
+ /** A directory's direct entries, in `readdir` order; none if it cannot be read. */
118
+ private async readChildren(dir: string): Promise<FileInfo[]> {
119
+ let entries: Dirent[];
116
120
  try {
117
- const entries = await fs.readdir(realPath, { withFileTypes: true });
118
-
119
- for (const entry of entries) {
120
- const entryPath = joinPath(normalizedPath, entry.name);
121
- const fullPath = this.resolvePath(entryPath);
122
-
123
- let stat: Stats;
124
- try {
125
- stat = await fs.stat(fullPath);
126
- } catch {
127
- continue;
128
- }
129
-
130
- const info: FileInfo = entry.isDirectory()
121
+ entries = await fs.readdir(this.resolvePath(dir), { withFileTypes: true });
122
+ } catch {
123
+ // Directory doesn't exist or can't be read
124
+ return [];
125
+ }
126
+ const infos: FileInfo[] = [];
127
+ for (const entry of entries) {
128
+ const entryPath = joinPath(dir, entry.name);
129
+ let stat: Stats;
130
+ try {
131
+ stat = await fs.stat(this.resolvePath(entryPath));
132
+ } catch {
133
+ continue;
134
+ }
135
+ infos.push(
136
+ entry.isDirectory()
131
137
  ? { name: entry.name, path: entryPath, kind: "directory" }
132
138
  : {
133
139
  name: entry.name,
@@ -135,18 +141,10 @@ export class NodeFilesApi implements FilesApi {
135
141
  kind: "file",
136
142
  size: stat.size,
137
143
  lastModified: stat.mtimeMs,
138
- };
139
-
140
- yield info;
141
-
142
- if (options?.recursive && entry.isDirectory()) {
143
- yield* this.list(entryPath, options);
144
- }
145
- }
146
- } catch {
147
- // Directory doesn't exist or can't be read
148
- return;
144
+ },
145
+ );
149
146
  }
147
+ return infos;
150
148
  }
151
149
 
152
150
  async stats(path: string): Promise<FileStats | undefined> {