@statewalker/webrun-files-node 0.7.0 → 0.10.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 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-'));
@@ -1,4 +1,5 @@
1
- //#region rolldown:runtime
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -6,30 +7,107 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
7
  var __getProtoOf = Object.getPrototypeOf;
7
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
9
  var __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") {
10
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
- key = keys[i];
12
- if (!__hasOwnProp.call(to, key) && key !== except) {
13
- __defProp(to, key, {
14
- get: ((k) => from[k]).bind(null, key),
15
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
- });
17
- }
18
- }
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
19
16
  }
20
17
  return to;
21
18
  };
22
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
23
20
  value: mod,
24
21
  enumerable: true
25
22
  }) : target, mod));
26
-
27
23
  //#endregion
28
24
  let node_fs_promises = require("node:fs/promises");
29
- node_fs_promises = __toESM(node_fs_promises);
30
-
25
+ node_fs_promises = __toESM(node_fs_promises, 1);
31
26
  //#region ../webrun-files/dist/esm/index.js
32
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
+ /**
33
111
  * Path manipulation utilities for virtual filesystem paths.
34
112
  * All paths use forward slashes and start with "/".
35
113
  */
@@ -60,7 +138,6 @@ function dirname(path) {
60
138
  if (lastSlash <= 0) return "/";
61
139
  return normalized.substring(0, lastSlash);
62
140
  }
63
-
64
141
  //#endregion
65
142
  //#region src/node-files-api.ts
66
143
  /**
@@ -129,39 +206,48 @@ var NodeFilesApi = class {
129
206
  const realPath = this.resolvePath(path);
130
207
  await node_fs_promises.mkdir(realPath, { recursive: true });
131
208
  }
209
+ /** `readdir` order is unspecified, so the tree is walked in `comparePaths` order. */
132
210
  async *list(path, options) {
133
- const realPath = this.resolvePath(path);
134
- 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;
135
216
  try {
136
- const entries = await node_fs_promises.readdir(realPath, { withFileTypes: true });
137
- for (const entry of entries) {
138
- const entryPath = joinPath(normalizedPath, entry.name);
139
- const fullPath = this.resolvePath(entryPath);
140
- let stat;
141
- try {
142
- stat = await node_fs_promises.stat(fullPath);
143
- } catch {
144
- continue;
145
- }
146
- yield {
147
- name: entry.name,
148
- path: entryPath,
149
- kind: entry.isDirectory() ? "directory" : "file",
150
- size: stat.size,
151
- lastModified: stat.mtimeMs
152
- };
153
- if (options?.recursive && entry.isDirectory()) yield* this.list(entryPath, options);
154
- }
217
+ entries = await node_fs_promises.readdir(this.resolvePath(dir), { withFileTypes: true });
155
218
  } catch {
156
- 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
+ });
157
241
  }
242
+ return infos;
158
243
  }
159
244
  async stats(path) {
160
245
  const realPath = this.resolvePath(path);
161
246
  try {
162
247
  const stat = await node_fs_promises.stat(realPath);
248
+ if (stat.isDirectory()) return { kind: "directory" };
163
249
  return {
164
- kind: stat.isDirectory() ? "directory" : "file",
250
+ kind: "file",
165
251
  size: stat.size,
166
252
  lastModified: stat.mtimeMs
167
253
  };
@@ -228,6 +314,5 @@ var NodeFilesApi = class {
228
314
  }
229
315
  }
230
316
  };
231
-
232
317
  //#endregion
233
- exports.NodeFilesApi = NodeFilesApi;
318
+ exports.NodeFilesApi = NodeFilesApi;
package/dist/esm/index.js CHANGED
@@ -1,7 +1,89 @@
1
1
  import * as fs from "node:fs/promises";
2
-
3
2
  //#region ../webrun-files/dist/esm/index.js
4
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
+ /**
5
87
  * Path manipulation utilities for virtual filesystem paths.
6
88
  * All paths use forward slashes and start with "/".
7
89
  */
@@ -32,7 +114,6 @@ function dirname(path) {
32
114
  if (lastSlash <= 0) return "/";
33
115
  return normalized.substring(0, lastSlash);
34
116
  }
35
-
36
117
  //#endregion
37
118
  //#region src/node-files-api.ts
38
119
  /**
@@ -101,39 +182,48 @@ var NodeFilesApi = class {
101
182
  const realPath = this.resolvePath(path);
102
183
  await fs.mkdir(realPath, { recursive: true });
103
184
  }
185
+ /** `readdir` order is unspecified, so the tree is walked in `comparePaths` order. */
104
186
  async *list(path, options) {
105
- const realPath = this.resolvePath(path);
106
- 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;
107
192
  try {
108
- const entries = await fs.readdir(realPath, { withFileTypes: true });
109
- for (const entry of entries) {
110
- const entryPath = joinPath(normalizedPath, entry.name);
111
- const fullPath = this.resolvePath(entryPath);
112
- let stat;
113
- try {
114
- stat = await fs.stat(fullPath);
115
- } catch {
116
- continue;
117
- }
118
- yield {
119
- name: entry.name,
120
- path: entryPath,
121
- kind: entry.isDirectory() ? "directory" : "file",
122
- size: stat.size,
123
- lastModified: stat.mtimeMs
124
- };
125
- if (options?.recursive && entry.isDirectory()) yield* this.list(entryPath, options);
126
- }
193
+ entries = await fs.readdir(this.resolvePath(dir), { withFileTypes: true });
127
194
  } catch {
128
- 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
+ });
129
217
  }
218
+ return infos;
130
219
  }
131
220
  async stats(path) {
132
221
  const realPath = this.resolvePath(path);
133
222
  try {
134
223
  const stat = await fs.stat(realPath);
224
+ if (stat.isDirectory()) return { kind: "directory" };
135
225
  return {
136
- kind: stat.isDirectory() ? "directory" : "file",
226
+ kind: "file",
137
227
  size: stat.size,
138
228
  lastModified: stat.mtimeMs
139
229
  };
@@ -200,6 +290,5 @@ var NodeFilesApi = class {
200
290
  }
201
291
  }
202
292
  };
203
-
204
293
  //#endregion
205
- export { NodeFilesApi };
294
+ export { NodeFilesApi };
@@ -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;gBAEZ,OAAO,CAAC,EAAE,mBAAmB;IAIzC,OAAO,CAAC,WAAW;IAKZ,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC;IA2CrE,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC;IAyBV,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC;IAsCnE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAenD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWtC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBtC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqBtD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAoB7D"}
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.7.0",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Node.js FilesApi implementation",
@@ -29,15 +29,15 @@
29
29
  "src"
30
30
  ],
31
31
  "dependencies": {
32
- "@statewalker/webrun-files": "0.7.0"
32
+ "@statewalker/webrun-files": "0.10.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/node": "^24.10.1",
36
- "rimraf": "^6.1.2",
37
- "rolldown": "^1.0.0-beta.53",
38
- "typescript": "^5.6.0",
39
- "vitest": "^4.0.12",
40
- "@statewalker/webrun-files-tests": "1.0.0"
35
+ "@types/node": "^26.2.0",
36
+ "rimraf": "^6.1.3",
37
+ "rolldown": "^1.2.4",
38
+ "typescript": "^7.0.2",
39
+ "vitest": "^4.1.10",
40
+ "@statewalker/webrun-files-tests": "3.0.0"
41
41
  },
42
42
  "sideEffects": false,
43
43
  "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,42 +109,42 @@ 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 = {
131
- name: entry.name,
132
- path: entryPath,
133
- kind: entry.isDirectory() ? "directory" : "file",
134
- size: stat.size,
135
- lastModified: stat.mtimeMs,
136
- };
137
-
138
- yield info;
139
-
140
- if (options?.recursive && entry.isDirectory()) {
141
- yield* this.list(entryPath, options);
142
- }
143
- }
121
+ entries = await fs.readdir(this.resolvePath(dir), { withFileTypes: true });
144
122
  } catch {
145
123
  // Directory doesn't exist or can't be read
146
- return;
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()
137
+ ? { name: entry.name, path: entryPath, kind: "directory" }
138
+ : {
139
+ name: entry.name,
140
+ path: entryPath,
141
+ kind: "file",
142
+ size: stat.size,
143
+ lastModified: stat.mtimeMs,
144
+ },
145
+ );
147
146
  }
147
+ return infos;
148
148
  }
149
149
 
150
150
  async stats(path: string): Promise<FileStats | undefined> {
@@ -152,8 +152,13 @@ export class NodeFilesApi implements FilesApi {
152
152
 
153
153
  try {
154
154
  const stat = await fs.stat(realPath);
155
+ // A directory reports neither size nor time. `stat` supplies both, but
156
+ // the size is the inode's own bookkeeping rather than anything about the
157
+ // contents, and neither is available on every storage, so neither is
158
+ // part of what a consumer may rely on.
159
+ if (stat.isDirectory()) return { kind: "directory" };
155
160
  return {
156
- kind: stat.isDirectory() ? "directory" : "file",
161
+ kind: "file",
157
162
  size: stat.size,
158
163
  lastModified: stat.mtimeMs,
159
164
  };