@statewalker/webrun-files-browser 0.7.2 → 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 +11 -3
- package/dist/browser-files-api.d.ts +3 -0
- package/dist/browser-files-api.d.ts.map +1 -1
- package/dist/cjs/index.cjs +31 -25
- package/dist/esm/index.js +32 -26
- package/package.json +7 -7
- package/src/browser-files-api.ts +40 -38
package/README.md
CHANGED
|
@@ -115,7 +115,8 @@ class BrowserFilesApi implements FilesApi {
|
|
|
115
115
|
### Helper Functions
|
|
116
116
|
|
|
117
117
|
```typescript
|
|
118
|
-
//
|
|
118
|
+
// Returns a BrowserFilesApi for the stored handle, or for one chosen in a directory picker;
|
|
119
|
+
// throws if permission is denied or the directory is no longer accessible
|
|
119
120
|
function openBrowserFilesApi(options?: OpenBrowserFilesApiOptions): Promise<BrowserFilesApi>;
|
|
120
121
|
|
|
121
122
|
interface OpenBrowserFilesApiOptions {
|
|
@@ -141,9 +142,16 @@ function verifyPermission(
|
|
|
141
142
|
): Promise<boolean>;
|
|
142
143
|
```
|
|
143
144
|
|
|
145
|
+
## Ordered Listings
|
|
146
|
+
|
|
147
|
+
Directory handles enumerate in no defined order, so `list()` reads each directory (fetching each
|
|
148
|
+
file's `File` for its size and time) and yields entries in path order, as every `FilesApi` does. A
|
|
149
|
+
recursive listing reads a directory only when the listing reaches it, and with `{ after }` skips any
|
|
150
|
+
directory whose whole subtree sorts at or before the cursor.
|
|
151
|
+
|
|
144
152
|
## Native Move Support
|
|
145
153
|
|
|
146
|
-
The `move()` method attempts to use the browser's native [`FileSystemHandle.move()`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/move) when available (Chrome 110+, Edge 110+). This provides an atomic, in-place rename/move without copying file data. On older browsers or polyfills that lack native `move()`, the method falls back to copy-then-delete.
|
|
154
|
+
The `move()` method attempts to use the browser's native [`FileSystemHandle.move()`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/move) when available (Chrome 110+, Edge 110+). This provides an atomic, in-place rename/move without copying file data. On older browsers or polyfills that lack native `move()`, or when the native call throws, the method falls back to copy-then-delete, which is not atomic.
|
|
147
155
|
|
|
148
156
|
## Browser Support
|
|
149
157
|
|
|
@@ -169,7 +177,7 @@ When working with persistent directory handles:
|
|
|
169
177
|
```typescript
|
|
170
178
|
import { verifyPermission, isHandlerAccessible } from '@statewalker/webrun-files-browser';
|
|
171
179
|
|
|
172
|
-
// Check if
|
|
180
|
+
// Check read-write permission, prompting the user if it is not already granted (needs a user gesture)
|
|
173
181
|
const hasPermission = await verifyPermission(directoryHandle, true);
|
|
174
182
|
|
|
175
183
|
// Check if the directory still exists and is accessible
|
|
@@ -27,7 +27,10 @@ export declare class BrowserFilesApi implements FilesApi {
|
|
|
27
27
|
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
|
|
28
28
|
write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
|
|
29
29
|
mkdir(path: string): Promise<void>;
|
|
30
|
+
/** Directory handles enumerate in no defined order, so the tree is walked in `comparePaths` order. */
|
|
30
31
|
list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
|
|
32
|
+
/** A directory's direct entries, in handle order; none if it does not exist. */
|
|
33
|
+
private readChildren;
|
|
31
34
|
stats(path: string): Promise<FileStats | undefined>;
|
|
32
35
|
exists(path: string): Promise<boolean>;
|
|
33
36
|
remove(path: string): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-files-api.d.ts","sourceRoot":"","sources":["../src/browser-files-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"browser-files-api.d.ts","sourceRoot":"","sources":["../src/browser-files-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAcnC,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,UAAU,EAAE,yBAAyB,CAAC;CACvC;AAED;;;;;;GAMG;AACH,qBAAa,eAAgB,YAAW,QAAQ;IAC9C,OAAO,CAAC,UAAU,CAA4B;IAE9C,YAAY,OAAO,EAAE,sBAAsB,EAE1C;YAEa,kBAAkB;YAkBlB,aAAa;IAkBpB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC,CA2B1E;IAEK,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAoBf;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGvC;IAED,sGAAsG;IAC/F,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC,CAExE;IAED,gFAAgF;YAClE,YAAY;IAiCpB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CA6BxD;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG3C;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,CA8B3D;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA2E3D;CACF;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,eAAe,CAAC,CAGhE"}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -75,35 +75,44 @@ var BrowserFilesApi = class {
|
|
|
75
75
|
const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
|
|
76
76
|
await this.getDirectoryHandle(normalized, { create: true });
|
|
77
77
|
}
|
|
78
|
+
/** Directory handles enumerate in no defined order, so the tree is walked in `comparePaths` order. */
|
|
78
79
|
async *list(path, options) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
yield* (0, _statewalker_webrun_files.listInPathOrder)((0, _statewalker_webrun_files.normalizePath)(path), (dir) => this.readChildren(dir), options);
|
|
81
|
+
}
|
|
82
|
+
/** A directory's direct entries, in handle order; none if it does not exist. */
|
|
83
|
+
async readChildren(dir) {
|
|
84
|
+
const dirHandle = await this.getDirectoryHandle(dir);
|
|
85
|
+
if (!dirHandle) return [];
|
|
86
|
+
const infos = [];
|
|
83
87
|
for await (const [name, handle] of dirHandle.entries()) {
|
|
84
|
-
const entryPath = (0, _statewalker_webrun_files.joinPath)(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
const entryPath = (0, _statewalker_webrun_files.joinPath)(dir, name);
|
|
89
|
+
if (handle.kind === "directory") {
|
|
90
|
+
infos.push({
|
|
91
|
+
kind: "directory",
|
|
92
|
+
name,
|
|
93
|
+
path: entryPath
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let file;
|
|
98
|
+
try {
|
|
99
|
+
file = await handle.getFile();
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
infos.push({
|
|
104
|
+
kind: "file",
|
|
88
105
|
name,
|
|
89
106
|
path: entryPath,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const file = await handle.getFile();
|
|
94
|
-
info.size = file.size;
|
|
95
|
-
info.lastModified = file.lastModified;
|
|
96
|
-
} catch {}
|
|
97
|
-
yield info;
|
|
98
|
-
if (recursive && isDirectory) yield* this.list(entryPath, options);
|
|
107
|
+
size: file.size,
|
|
108
|
+
lastModified: file.lastModified
|
|
109
|
+
});
|
|
99
110
|
}
|
|
111
|
+
return infos;
|
|
100
112
|
}
|
|
101
113
|
async stats(path) {
|
|
102
114
|
const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
|
|
103
|
-
if (normalized === "/") return {
|
|
104
|
-
kind: "directory",
|
|
105
|
-
lastModified: 0
|
|
106
|
-
};
|
|
115
|
+
if (normalized === "/") return { kind: "directory" };
|
|
107
116
|
const fileHandle = await this.getFileHandle(normalized);
|
|
108
117
|
if (fileHandle) try {
|
|
109
118
|
const f = await fileHandle.getFile();
|
|
@@ -115,10 +124,7 @@ var BrowserFilesApi = class {
|
|
|
115
124
|
} catch {
|
|
116
125
|
return;
|
|
117
126
|
}
|
|
118
|
-
if (await this.getDirectoryHandle(normalized)) return {
|
|
119
|
-
kind: "directory",
|
|
120
|
-
lastModified: 0
|
|
121
|
-
};
|
|
127
|
+
if (await this.getDirectoryHandle(normalized)) return { kind: "directory" };
|
|
122
128
|
}
|
|
123
129
|
async exists(path) {
|
|
124
130
|
return await this.stats(path) !== void 0;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename, dirname, joinPath, normalizePath } from "@statewalker/webrun-files";
|
|
1
|
+
import { basename, dirname, joinPath, listInPathOrder, normalizePath } from "@statewalker/webrun-files";
|
|
2
2
|
//#region src/browser-files-api.ts
|
|
3
3
|
/**
|
|
4
4
|
* Browser filesystem implementation using the File System Access API.
|
|
@@ -74,35 +74,44 @@ var BrowserFilesApi = class {
|
|
|
74
74
|
const normalized = normalizePath(path);
|
|
75
75
|
await this.getDirectoryHandle(normalized, { create: true });
|
|
76
76
|
}
|
|
77
|
+
/** Directory handles enumerate in no defined order, so the tree is walked in `comparePaths` order. */
|
|
77
78
|
async *list(path, options) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
yield* listInPathOrder(normalizePath(path), (dir) => this.readChildren(dir), options);
|
|
80
|
+
}
|
|
81
|
+
/** A directory's direct entries, in handle order; none if it does not exist. */
|
|
82
|
+
async readChildren(dir) {
|
|
83
|
+
const dirHandle = await this.getDirectoryHandle(dir);
|
|
84
|
+
if (!dirHandle) return [];
|
|
85
|
+
const infos = [];
|
|
82
86
|
for await (const [name, handle] of dirHandle.entries()) {
|
|
83
|
-
const entryPath = joinPath(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
const entryPath = joinPath(dir, name);
|
|
88
|
+
if (handle.kind === "directory") {
|
|
89
|
+
infos.push({
|
|
90
|
+
kind: "directory",
|
|
91
|
+
name,
|
|
92
|
+
path: entryPath
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
let file;
|
|
97
|
+
try {
|
|
98
|
+
file = await handle.getFile();
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
infos.push({
|
|
103
|
+
kind: "file",
|
|
87
104
|
name,
|
|
88
105
|
path: entryPath,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const file = await handle.getFile();
|
|
93
|
-
info.size = file.size;
|
|
94
|
-
info.lastModified = file.lastModified;
|
|
95
|
-
} catch {}
|
|
96
|
-
yield info;
|
|
97
|
-
if (recursive && isDirectory) yield* this.list(entryPath, options);
|
|
106
|
+
size: file.size,
|
|
107
|
+
lastModified: file.lastModified
|
|
108
|
+
});
|
|
98
109
|
}
|
|
110
|
+
return infos;
|
|
99
111
|
}
|
|
100
112
|
async stats(path) {
|
|
101
113
|
const normalized = normalizePath(path);
|
|
102
|
-
if (normalized === "/") return {
|
|
103
|
-
kind: "directory",
|
|
104
|
-
lastModified: 0
|
|
105
|
-
};
|
|
114
|
+
if (normalized === "/") return { kind: "directory" };
|
|
106
115
|
const fileHandle = await this.getFileHandle(normalized);
|
|
107
116
|
if (fileHandle) try {
|
|
108
117
|
const f = await fileHandle.getFile();
|
|
@@ -114,10 +123,7 @@ var BrowserFilesApi = class {
|
|
|
114
123
|
} catch {
|
|
115
124
|
return;
|
|
116
125
|
}
|
|
117
|
-
if (await this.getDirectoryHandle(normalized)) return {
|
|
118
|
-
kind: "directory",
|
|
119
|
-
lastModified: 0
|
|
120
|
-
};
|
|
126
|
+
if (await this.getDirectoryHandle(normalized)) return { kind: "directory" };
|
|
121
127
|
}
|
|
122
128
|
async exists(path) {
|
|
123
129
|
return await this.stats(path) !== void 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-files-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser File System Access API implementation for webrun-files",
|
|
@@ -29,16 +29,16 @@
|
|
|
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": "^
|
|
35
|
+
"@types/node": "^26.2.0",
|
|
36
36
|
"native-file-system-adapter": "^3.0.1",
|
|
37
37
|
"rimraf": "^6.1.3",
|
|
38
|
-
"rolldown": "^1.
|
|
39
|
-
"typescript": "^
|
|
40
|
-
"vitest": "^4.1.
|
|
41
|
-
"@statewalker/webrun-files-tests": "
|
|
38
|
+
"rolldown": "^1.2.4",
|
|
39
|
+
"typescript": "^7.0.2",
|
|
40
|
+
"vitest": "^4.1.10",
|
|
41
|
+
"@statewalker/webrun-files-tests": "3.0.0"
|
|
42
42
|
},
|
|
43
43
|
"sideEffects": false,
|
|
44
44
|
"publishConfig": {
|
package/src/browser-files-api.ts
CHANGED
|
@@ -12,7 +12,13 @@ import type {
|
|
|
12
12
|
ListOptions,
|
|
13
13
|
ReadOptions,
|
|
14
14
|
} from "@statewalker/webrun-files";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
basename,
|
|
17
|
+
dirname,
|
|
18
|
+
joinPath,
|
|
19
|
+
listInPathOrder,
|
|
20
|
+
normalizePath,
|
|
21
|
+
} from "@statewalker/webrun-files";
|
|
16
22
|
|
|
17
23
|
/** Chrome 110+ exposes move() on FileSystemHandle; not yet in TS DOM types. */
|
|
18
24
|
interface MovableHandle extends FileSystemHandle {
|
|
@@ -136,51 +142,50 @@ export class BrowserFilesApi implements FilesApi {
|
|
|
136
142
|
await this.getDirectoryHandle(normalized, { create: true });
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/** Directory handles enumerate in no defined order, so the tree is walked in `comparePaths` order. */
|
|
139
146
|
async *list(path: string, options?: ListOptions): AsyncIterable<FileInfo> {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!dirHandle) return;
|
|
143
|
-
|
|
144
|
-
const recursive = options?.recursive ?? false;
|
|
147
|
+
yield* listInPathOrder(normalizePath(path), (dir) => this.readChildren(dir), options);
|
|
148
|
+
}
|
|
145
149
|
|
|
150
|
+
/** A directory's direct entries, in handle order; none if it does not exist. */
|
|
151
|
+
private async readChildren(dir: string): Promise<FileInfo[]> {
|
|
152
|
+
const dirHandle = await this.getDirectoryHandle(dir);
|
|
153
|
+
if (!dirHandle) return [];
|
|
154
|
+
const infos: FileInfo[] = [];
|
|
146
155
|
for await (const [name, handle] of dirHandle.entries()) {
|
|
147
|
-
const entryPath = joinPath(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
kind: isDirectory ? "directory" : "file",
|
|
152
|
-
name,
|
|
153
|
-
path: entryPath,
|
|
154
|
-
lastModified: 0,
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
if (!isDirectory) {
|
|
158
|
-
try {
|
|
159
|
-
const fileHandle = handle as FileSystemFileHandle;
|
|
160
|
-
const file = await fileHandle.getFile();
|
|
161
|
-
info.size = file.size;
|
|
162
|
-
info.lastModified = file.lastModified;
|
|
163
|
-
} catch {
|
|
164
|
-
// Ignore errors getting file metadata
|
|
165
|
-
}
|
|
156
|
+
const entryPath = joinPath(dir, name);
|
|
157
|
+
if (handle.kind === "directory") {
|
|
158
|
+
infos.push({ kind: "directory", name, path: entryPath });
|
|
159
|
+
continue;
|
|
166
160
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
161
|
+
// A file must report both a size and a time, so the `File` behind the
|
|
162
|
+
// handle has to be obtained before the entry can be listed at all.
|
|
163
|
+
// `getFile()` rejects when the entry was removed between the directory
|
|
164
|
+
// read and this call, or when permission was revoked mid-iteration;
|
|
165
|
+
// either way there is no file left to describe, and skipping it is
|
|
166
|
+
// the same answer a listing taken a moment later would have given.
|
|
167
|
+
let file: File;
|
|
168
|
+
try {
|
|
169
|
+
file = await (handle as FileSystemFileHandle).getFile();
|
|
170
|
+
} catch {
|
|
171
|
+
continue;
|
|
172
172
|
}
|
|
173
|
+
infos.push({
|
|
174
|
+
kind: "file",
|
|
175
|
+
name,
|
|
176
|
+
path: entryPath,
|
|
177
|
+
size: file.size,
|
|
178
|
+
lastModified: file.lastModified,
|
|
179
|
+
});
|
|
173
180
|
}
|
|
181
|
+
return infos;
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
async stats(path: string): Promise<FileStats | undefined> {
|
|
177
185
|
const normalized = normalizePath(path);
|
|
178
186
|
|
|
179
187
|
if (normalized === "/") {
|
|
180
|
-
return {
|
|
181
|
-
kind: "directory",
|
|
182
|
-
lastModified: 0,
|
|
183
|
-
};
|
|
188
|
+
return { kind: "directory" };
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
// Try as file first
|
|
@@ -201,10 +206,7 @@ export class BrowserFilesApi implements FilesApi {
|
|
|
201
206
|
// Try as directory
|
|
202
207
|
const dirHandle = await this.getDirectoryHandle(normalized);
|
|
203
208
|
if (dirHandle) {
|
|
204
|
-
return {
|
|
205
|
-
kind: "directory",
|
|
206
|
-
lastModified: 0,
|
|
207
|
-
};
|
|
209
|
+
return { kind: "directory" };
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
return undefined;
|