@statewalker/webrun-files-browser 0.10.1 → 0.10.3

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.
Files changed (2) hide show
  1. package/README.md +85 -150
  2. package/package.json +9 -5
package/README.md CHANGED
@@ -1,210 +1,145 @@
1
1
  # @statewalker/webrun-files-browser
2
2
 
3
- Browser File System Access API implementation of the `FilesApi` interface from `@statewalker/webrun-files`.
3
+ ## What it is
4
4
 
5
- ## Overview
5
+ `BrowserFilesApi`, an implementation of `FilesApi` from `@statewalker/webrun-files` over a
6
+ [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API)
7
+ directory handle, plus helpers to obtain one: a user-picked directory (with optional persistence
8
+ of the handle) or the Origin Private File System (OPFS).
6
9
 
7
- This package provides a `FilesApi` implementation that works in modern browsers using the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API). It allows web applications to read and write files to:
10
+ ## Why it exists
8
11
 
9
- - **User-selected directories** via `showDirectoryPicker()`
10
- - **Origin Private File System (OPFS)** - a sandboxed filesystem that persists between sessions
12
+ Web apps that edit local projects, or need private persistent storage, get a
13
+ `FileSystemDirectoryHandle` from the browser, whose API is handle-based, unordered and full of
14
+ permission states. This package turns the handle into the same `FilesApi` the rest of the code
15
+ already uses, and wraps the picker, permission and stale-handle checks a real app needs around it.
11
16
 
12
- ## Installation
17
+ ## How to use
13
18
 
14
19
  ```bash
15
- npm install @statewalker/webrun-files-browser @statewalker/webrun-files
20
+ pnpm add @statewalker/webrun-files-browser @statewalker/webrun-files
16
21
  ```
17
22
 
18
- ## Usage
23
+ One entry point, `@statewalker/webrun-files-browser`: ESM (`dist/esm/index.js`), CommonJS
24
+ (`dist/cjs/index.cjs`), types (`dist/index.d.ts`); sources in `src/`. Browsers with the File
25
+ System Access API; requires a secure context (HTTPS or localhost).
19
26
 
20
- ### With User-Selected Directory
27
+ | Export | What it gives |
28
+ | --- | --- |
29
+ | `new BrowserFilesApi({ rootHandle })` | A `FilesApi` over any `FileSystemDirectoryHandle` |
30
+ | `openBrowserFilesApi(options?)` | Reuses a stored handle or shows `showDirectoryPicker()`, checks permission and accessibility |
31
+ | `getOPFSFilesApi()` | A `BrowserFilesApi` over `navigator.storage.getDirectory()` |
32
+ | `verifyPermission(handle, readWrite = false)` | Queries, then requests, `read` or `readwrite` permission |
33
+ | `isHandlerAccessible(handle)` | Whether the handle still points at something readable |
34
+
35
+ `OpenBrowserFilesApiOptions`:
36
+
37
+ | Option | Default | |
38
+ | --- | --- | --- |
39
+ | `handlerKey` | `"root-dir"` | Key under which the handle is stored |
40
+ | `readwrite` | `true` | Request read-write (`true`) or read-only access |
41
+ | `get(key)` | returns `undefined` | Load a stored handle |
42
+ | `set(key, handle)` | no-op | Store a newly picked handle |
43
+ | `del(key)` | no-op | Forget a handle that is no longer accessible |
44
+
45
+ ## Examples
46
+
47
+ ### A user-picked directory
21
48
 
22
49
  ```typescript
23
50
  import { openBrowserFilesApi } from '@statewalker/webrun-files-browser';
24
51
  import { readText, writeText } from '@statewalker/webrun-files';
25
52
 
26
- // Opens a directory picker dialog (requires user gesture)
53
+ // Call from a click handler: showDirectoryPicker() needs a user gesture
27
54
  const files = await openBrowserFilesApi();
28
55
 
29
- // Write a file
30
56
  await writeText(files, '/notes/hello.txt', 'Hello, World!');
31
-
32
- // Read a file
33
- const content = await readText(files, '/notes/hello.txt');
34
- console.log(content); // "Hello, World!"
35
-
36
- // List directory contents
37
- for await (const entry of files.list('/notes')) {
38
- console.log(entry.name, entry.kind);
39
- }
57
+ console.log(await readText(files, '/notes/hello.txt'));
40
58
  ```
41
59
 
42
- ### With Persistent Directory Handle
43
-
44
- Persist the directory handle using IndexedDB (via `idb-keyval` or similar) to avoid prompting the user every time:
60
+ ### Remember the directory across sessions
45
61
 
46
62
  ```typescript
47
63
  import { openBrowserFilesApi } from '@statewalker/webrun-files-browser';
48
- import { get, set, del } from 'idb-keyval';
49
-
50
- // Opens directory picker if no stored handle, otherwise reuses stored one
51
- const files = await openBrowserFilesApi({
52
- handlerKey: 'my-project-dir', // Key for storing the handle
53
- readwrite: true, // Request read-write access
54
- get, // Retrieve stored handle
55
- set, // Store the handle
56
- del, // Delete stored handle if inaccessible
57
- });
58
- ```
64
+ import { del, get, set } from 'idb-keyval';
59
65
 
60
- ### With Origin Private File System (OPFS)
66
+ const files = await openBrowserFilesApi({ handlerKey: 'my-project-dir', readwrite: true, get, set, del });
67
+ ```
61
68
 
62
- OPFS provides a sandboxed filesystem that doesn't require user interaction:
69
+ ### OPFS, no prompt
63
70
 
64
71
  ```typescript
65
72
  import { getOPFSFilesApi } from '@statewalker/webrun-files-browser';
66
73
  import { writeText } from '@statewalker/webrun-files';
67
74
 
68
- // No user gesture required
69
75
  const files = await getOPFSFilesApi();
70
-
71
- // Use the same API
72
76
  await writeText(files, '/data/config.json', '{"theme": "dark"}');
73
77
  ```
74
78
 
75
- ### With Custom Directory Handle
76
-
77
- Create a `BrowserFilesApi` instance with any `FileSystemDirectoryHandle`:
78
-
79
- ```typescript
80
- import { BrowserFilesApi } from '@statewalker/webrun-files-browser';
81
-
82
- // Get a directory handle from drag & drop, IndexedDB, etc.
83
- const directoryHandle = await getDirectoryHandleSomehow();
84
-
85
- const files = new BrowserFilesApi({ rootHandle: directoryHandle });
86
- ```
87
-
88
- ## API Reference
89
-
90
- ### BrowserFilesApi
91
-
92
- Main class implementing `FilesApi` for browser environments.
79
+ ### Any handle (drag and drop, IndexedDB)
93
80
 
94
81
  ```typescript
95
- interface BrowserFilesApiOptions {
96
- rootHandle: FileSystemDirectoryHandle;
97
- }
82
+ import { BrowserFilesApi, isHandlerAccessible, verifyPermission } from '@statewalker/webrun-files-browser';
98
83
 
99
- class BrowserFilesApi implements FilesApi {
100
- constructor(options: BrowserFilesApiOptions);
101
-
102
- // All FilesApi methods
103
- read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
104
- write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
105
- mkdir(path: string): Promise<void>;
106
- list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
107
- stats(path: string): Promise<FileStats | undefined>;
108
- exists(path: string): Promise<boolean>;
109
- remove(path: string): Promise<boolean>;
110
- move(source: string, target: string): Promise<boolean>; // uses native move when available
111
- copy(source: string, target: string): Promise<boolean>;
84
+ if ((await verifyPermission(handle, true)) && (await isHandlerAccessible(handle))) {
85
+ const files = new BrowserFilesApi({ rootHandle: handle });
112
86
  }
113
87
  ```
114
88
 
115
- ### Helper Functions
89
+ ### Testing in Node.js
116
90
 
117
91
  ```typescript
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
120
- function openBrowserFilesApi(options?: OpenBrowserFilesApiOptions): Promise<BrowserFilesApi>;
121
-
122
- interface OpenBrowserFilesApiOptions {
123
- handlerKey?: string; // Key for storing the handle (default: "root-dir")
124
- readwrite?: boolean; // Request read-write access (default: true)
125
- get?: (key: string) => Promise<FileSystemDirectoryHandle | undefined>;
126
- set?: (key: string, handler: FileSystemDirectoryHandle) => Promise<void>;
127
- del?: (key: string) => Promise<void>;
128
- }
129
-
130
- // Returns a BrowserFilesApi backed by the Origin Private File System
131
- function getOPFSFilesApi(): Promise<BrowserFilesApi>;
132
-
133
- // Checks if a file system handle is still accessible
134
- function isHandlerAccessible(
135
- fileHandle: FileSystemFileHandle | FileSystemDirectoryHandle
136
- ): Promise<boolean>;
92
+ import { getOriginPrivateDirectory } from 'native-file-system-adapter';
93
+ // @ts-expect-error - no type declarations
94
+ import * as driver from 'native-file-system-adapter/src/adapters/memory.js';
95
+ import { BrowserFilesApi } from '@statewalker/webrun-files-browser';
137
96
 
138
- // Verifies and requests file system permissions for a handle
139
- function verifyPermission(
140
- fileHandle: FileSystemFileHandle | FileSystemDirectoryHandle,
141
- readWrite?: boolean // default: false
142
- ): Promise<boolean>;
97
+ const files = new BrowserFilesApi({ rootHandle: await getOriginPrivateDirectory(driver) });
143
98
  ```
144
99
 
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
-
152
- ## Native Move Support
153
-
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.
155
-
156
- ## Browser Support
157
-
158
- The File System Access API is supported in:
159
-
160
- - Chrome/Edge 86+ (native `move()` from 110+)
161
- - Opera 72+
162
- - Chrome for Android 86+
163
-
164
- For other browsers, you may need a polyfill like [native-file-system-adapter](https://github.com/jimmywarting/native-file-system-adapter).
100
+ This is how the package's own tests run.
165
101
 
166
- ## Security Considerations
102
+ ## Internals
167
103
 
168
- - **User gesture required**: `showDirectoryPicker()` must be called from a user gesture (click, key press)
169
- - **Secure context**: The API only works in secure contexts (HTTPS or localhost)
170
- - **Permission prompts**: Users must grant permission to access directories
171
- - **OPFS isolation**: Origin Private File System is isolated per-origin and not accessible to the user
104
+ ### How calls map to handles
172
105
 
173
- ## Permission and Accessibility Utilities
106
+ - `read` slices the `File` from `getFile()` into 8 KiB `arrayBuffer()` reads over the requested range.
107
+ - `write` creates parents, then streams every chunk into `createWritable()` and closes it.
108
+ - `remove` is `removeEntry(name, { recursive: true })` on the parent.
109
+ - `move` uses the native `FileSystemHandle.move()` where it exists (Chromium 110+), which renames
110
+ in place without copying. Without it, or when it throws, `move` copies then removes, which is
111
+ not atomic.
174
112
 
175
- When working with persistent directory handles:
113
+ ### Why listings read every file
176
114
 
177
- ```typescript
178
- import { verifyPermission, isHandlerAccessible } from '@statewalker/webrun-files-browser';
179
-
180
- // Check read-write permission, prompting the user if it is not already granted (needs a user gesture)
181
- const hasPermission = await verifyPermission(directoryHandle, true);
182
-
183
- // Check if the directory still exists and is accessible
184
- const isAccessible = await isHandlerAccessible(directoryHandle);
115
+ Directory handles enumerate in no defined order, and every `FilesApi` must list in `comparePaths`
116
+ order. `list()` reads each directory, calls `getFile()` per file for its size and time, and yields
117
+ entries sorted. A recursive listing reads a directory only when the listing reaches it, and skips
118
+ subtrees that sort at or before `after`. A large directory costs one `getFile()` per file.
185
119
 
186
- if (!hasPermission) {
187
- console.log('Permission denied - user may need to re-grant access');
188
- }
120
+ ### What breaks, and how it looks
189
121
 
190
- if (!isAccessible) {
191
- console.log('Directory no longer accessible - may have been deleted or moved');
192
- }
193
- ```
122
+ - **`openBrowserFilesApi` throws** `Access was not granted` when the permission prompt is refused,
123
+ and `Cannot access the folder. Please try again.` when a stored handle points at a moved or
124
+ deleted directory (it calls `del(handlerKey)` first, so the next call shows the picker).
125
+ - **No user gesture, no picker.** Calling `openBrowserFilesApi()` without a stored handle outside a
126
+ click or key handler rejects with the browser's `SecurityError`.
127
+ - **A failed write still commits.** If the content iterable throws midway, `write` closes the
128
+ writable stream in a `finally`, which saves the bytes written so far instead of aborting.
129
+ - **Read errors look like missing files.** `read` yields nothing when the handle or file cannot be
130
+ read.
194
131
 
195
- ## Testing
132
+ ### Browser support
196
133
 
197
- For testing in Node.js environments, use [native-file-system-adapter](https://github.com/jimmywarting/native-file-system-adapter) which provides a memory backend:
134
+ The File System Access API with `showDirectoryPicker()` exists in Chromium-based browsers (Chrome,
135
+ Edge, Opera). OPFS (`getOPFSFilesApi`) is more widely available. For other environments, such as
136
+ tests, [native-file-system-adapter](https://github.com/jimmywarting/native-file-system-adapter)
137
+ provides compatible handles.
198
138
 
199
- ```typescript
200
- import { getOriginPrivateDirectory } from 'native-file-system-adapter';
201
- // @ts-expect-error - no type declarations
202
- import * as driver from 'native-file-system-adapter/src/adapters/memory.js';
203
- import { BrowserFilesApi } from '@statewalker/webrun-files-browser';
139
+ ### Dependencies
204
140
 
205
- const rootHandle = await getOriginPrivateDirectory(driver);
206
- const files = new BrowserFilesApi({ rootHandle });
207
- ```
141
+ `@statewalker/webrun-files` only. `native-file-system-adapter` is a development dependency for
142
+ tests.
208
143
 
209
144
  ## License
210
145
 
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-files-browser",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Browser File System Access API implementation for webrun-files",
7
- "homepage": "https://github.com/statewalker/webrun-files",
7
+ "bugs": {
8
+ "url": "https://github.com/statewalker/webrun-files/issues"
9
+ },
10
+ "homepage": "https://github.com/statewalker/webrun-files/tree/main/packages/webrun-files-browser#readme",
8
11
  "author": {
9
12
  "name": "Mikhail Kotelnikov",
10
13
  "email": "mikhail.kotelnikov@gmail.com"
@@ -12,7 +15,8 @@
12
15
  "license": "MIT",
13
16
  "repository": {
14
17
  "type": "git",
15
- "url": "git@github.com:statewalker/webrun-files.git"
18
+ "url": "git+https://github.com/statewalker/webrun-files.git",
19
+ "directory": "packages/webrun-files-browser"
16
20
  },
17
21
  "main": "./dist/cjs/index.cjs",
18
22
  "module": "./dist/esm/index.js",
@@ -30,7 +34,7 @@
30
34
  "src"
31
35
  ],
32
36
  "dependencies": {
33
- "@statewalker/webrun-files": "^0.10.1"
37
+ "@statewalker/webrun-files": "^0.10.4"
34
38
  },
35
39
  "devDependencies": {
36
40
  "@types/node": "^26.6.4",
@@ -38,7 +42,7 @@
38
42
  "rimraf": "^6.1.3",
39
43
  "rolldown": "^1.2.12",
40
44
  "typescript": "^7.0.2",
41
- "vitest": "^5.0.3",
45
+ "vitest": "^4.1.4 || ^5.0.3",
42
46
  "@statewalker/webrun-files-tests": "^3.0.0"
43
47
  },
44
48
  "sideEffects": false,