@statewalker/webrun-files-browser 0.9.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
@@ -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;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;IAEM,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC,CA0CxE;IAEK,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"}
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,38 +75,40 @@ 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
- let info;
87
- if (isDirectory) info = {
88
- kind: "directory",
89
- name,
90
- path: entryPath
91
- };
92
- else {
93
- let file;
94
- try {
95
- file = await handle.getFile();
96
- } catch {
97
- continue;
98
- }
99
- info = {
100
- kind: "file",
88
+ const entryPath = (0, _statewalker_webrun_files.joinPath)(dir, name);
89
+ if (handle.kind === "directory") {
90
+ infos.push({
91
+ kind: "directory",
101
92
  name,
102
- path: entryPath,
103
- size: file.size,
104
- lastModified: file.lastModified
105
- };
93
+ path: entryPath
94
+ });
95
+ continue;
106
96
  }
107
- yield info;
108
- if (recursive && isDirectory) yield* this.list(entryPath, options);
97
+ let file;
98
+ try {
99
+ file = await handle.getFile();
100
+ } catch {
101
+ continue;
102
+ }
103
+ infos.push({
104
+ kind: "file",
105
+ name,
106
+ path: entryPath,
107
+ size: file.size,
108
+ lastModified: file.lastModified
109
+ });
109
110
  }
111
+ return infos;
110
112
  }
111
113
  async stats(path) {
112
114
  const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
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,38 +74,40 @@ 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
- let info;
86
- if (isDirectory) info = {
87
- kind: "directory",
88
- name,
89
- path: entryPath
90
- };
91
- else {
92
- let file;
93
- try {
94
- file = await handle.getFile();
95
- } catch {
96
- continue;
97
- }
98
- info = {
99
- kind: "file",
87
+ const entryPath = joinPath(dir, name);
88
+ if (handle.kind === "directory") {
89
+ infos.push({
90
+ kind: "directory",
100
91
  name,
101
- path: entryPath,
102
- size: file.size,
103
- lastModified: file.lastModified
104
- };
92
+ path: entryPath
93
+ });
94
+ continue;
105
95
  }
106
- yield info;
107
- if (recursive && isDirectory) yield* this.list(entryPath, options);
96
+ let file;
97
+ try {
98
+ file = await handle.getFile();
99
+ } catch {
100
+ continue;
101
+ }
102
+ infos.push({
103
+ kind: "file",
104
+ name,
105
+ path: entryPath,
106
+ size: file.size,
107
+ lastModified: file.lastModified
108
+ });
108
109
  }
110
+ return infos;
109
111
  }
110
112
  async stats(path) {
111
113
  const normalized = normalizePath(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-files-browser",
3
- "version": "0.9.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,7 +29,7 @@
29
29
  "src"
30
30
  ],
31
31
  "dependencies": {
32
- "@statewalker/webrun-files": "^0.9.0"
32
+ "@statewalker/webrun-files": "^0.10.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^26.2.0",
@@ -38,7 +38,7 @@
38
38
  "rolldown": "^1.2.4",
39
39
  "typescript": "^7.0.2",
40
40
  "vitest": "^4.1.10",
41
- "@statewalker/webrun-files-tests": "2.0.0"
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,48 +142,43 @@ 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
- let info: FileInfo;
151
- if (isDirectory) {
152
- info = { kind: "directory", name, path: entryPath };
153
- } else {
154
- // A file must report both a size and a time, so the `File` behind the
155
- // handle has to be obtained before the entry can be yielded at all.
156
- // `getFile()` rejects when the entry was removed between the directory
157
- // read and this call, or when permission was revoked mid-iteration;
158
- // either way there is no file left to describe, and skipping it is
159
- // the same answer a listing taken a moment later would have given.
160
- let file: File;
161
- try {
162
- file = await (handle as FileSystemFileHandle).getFile();
163
- } catch {
164
- continue;
165
- }
166
- info = {
167
- kind: "file",
168
- name,
169
- path: entryPath,
170
- size: file.size,
171
- lastModified: file.lastModified,
172
- };
156
+ const entryPath = joinPath(dir, name);
157
+ if (handle.kind === "directory") {
158
+ infos.push({ kind: "directory", name, path: entryPath });
159
+ continue;
173
160
  }
174
-
175
- yield info;
176
-
177
- if (recursive && isDirectory) {
178
- 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;
179
172
  }
173
+ infos.push({
174
+ kind: "file",
175
+ name,
176
+ path: entryPath,
177
+ size: file.size,
178
+ lastModified: file.lastModified,
179
+ });
180
180
  }
181
+ return infos;
181
182
  }
182
183
 
183
184
  async stats(path: string): Promise<FileStats | undefined> {