@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 +19 -3
- package/dist/cjs/index.cjs +126 -41
- package/dist/esm/index.js +116 -27
- package/dist/node-files-api.d.ts +3 -0
- package/dist/node-files-api.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/node-files-api.ts +38 -33
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
|
-
|
|
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
|
-
*
|
|
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-'));
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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:
|
|
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 };
|
package/dist/node-files-api.d.ts
CHANGED
|
@@ -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;
|
|
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.
|
|
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.
|
|
32
|
+
"@statewalker/webrun-files": "0.10.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@types/node": "^
|
|
36
|
-
"rimraf": "^6.1.
|
|
37
|
-
"rolldown": "^1.
|
|
38
|
-
"typescript": "^
|
|
39
|
-
"vitest": "^4.
|
|
40
|
-
"@statewalker/webrun-files-tests": "
|
|
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": {
|
package/src/node-files-api.ts
CHANGED
|
@@ -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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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:
|
|
161
|
+
kind: "file",
|
|
157
162
|
size: stat.size,
|
|
158
163
|
lastModified: stat.mtimeMs,
|
|
159
164
|
};
|