@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 CHANGED
@@ -115,7 +115,8 @@ class BrowserFilesApi implements FilesApi {
115
115
  ### Helper Functions
116
116
 
117
117
  ```typescript
118
- // Opens a directory picker and returns a BrowserFilesApi instance
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 we have read-write permission
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;AAQnC,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,UAAU,EAAE,yBAAyB,CAAC;CACvC;AAED;;;;;;GAMG;AACH,qBAAa,eAAgB,YAAW,QAAQ;IAC9C,OAAO,CAAC,UAAU,CAA4B;gBAElC,OAAO,EAAE,sBAAsB;YAI7B,kBAAkB;YAkBlB,aAAa;IAkBpB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC;IA6BrE,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC;IAsBV,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;IAqCnE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAqCnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtC,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;IAgCtD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CA4E7D;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,eAAe,CAAC,CAGhE"}
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"}
@@ -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
- const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
80
- const dirHandle = await this.getDirectoryHandle(normalized);
81
- if (!dirHandle) return;
82
- const recursive = options?.recursive ?? false;
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)(normalized, name);
85
- const isDirectory = handle.kind === "directory";
86
- const info = {
87
- kind: isDirectory ? "directory" : "file",
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
- lastModified: 0
91
- };
92
- if (!isDirectory) try {
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
- const normalized = normalizePath(path);
79
- const dirHandle = await this.getDirectoryHandle(normalized);
80
- if (!dirHandle) return;
81
- const recursive = options?.recursive ?? false;
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(normalized, name);
84
- const isDirectory = handle.kind === "directory";
85
- const info = {
86
- kind: isDirectory ? "directory" : "file",
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
- lastModified: 0
90
- };
91
- if (!isDirectory) try {
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.7.2",
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.7.0"
32
+ "@statewalker/webrun-files": "^0.10.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/node": "^25.6.0",
35
+ "@types/node": "^26.2.0",
36
36
  "native-file-system-adapter": "^3.0.1",
37
37
  "rimraf": "^6.1.3",
38
- "rolldown": "^1.0.0-rc.16",
39
- "typescript": "^6.0.3",
40
- "vitest": "^4.1.4",
41
- "@statewalker/webrun-files-tests": "1.0.0"
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": {
@@ -12,7 +12,13 @@ import type {
12
12
  ListOptions,
13
13
  ReadOptions,
14
14
  } from "@statewalker/webrun-files";
15
- import { basename, dirname, joinPath, normalizePath } from "@statewalker/webrun-files";
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
- const normalized = normalizePath(path);
141
- const dirHandle = await this.getDirectoryHandle(normalized);
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(normalized, name);
148
- const isDirectory = handle.kind === "directory";
149
-
150
- const info: FileInfo = {
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
- yield info;
169
-
170
- if (recursive && isDirectory) {
171
- yield* this.list(entryPath, options);
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;