@statewalker/webrun-files 0.5.0 → 0.9.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.
Files changed (42) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +131 -114
  3. package/dist/cjs/index.cjs +85 -529
  4. package/dist/esm/index.js +78 -519
  5. package/dist/file-utils.d.ts +47 -0
  6. package/dist/file-utils.d.ts.map +1 -0
  7. package/dist/index.d.ts +14 -8
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/{utils/path-utils.d.ts → path-utils.d.ts} +10 -1
  10. package/dist/path-utils.d.ts.map +1 -0
  11. package/dist/types.d.ts +130 -51
  12. package/dist/types.d.ts.map +1 -1
  13. package/package.json +7 -10
  14. package/src/file-utils.ts +106 -0
  15. package/src/index.ts +38 -27
  16. package/src/{utils/path-utils.ts → path-utils.ts} +16 -2
  17. package/src/types.ts +136 -53
  18. package/dist/files-api.d.ts +0 -44
  19. package/dist/files-api.d.ts.map +0 -1
  20. package/dist/impl/index.d.ts +0 -6
  21. package/dist/impl/index.d.ts.map +0 -1
  22. package/dist/impl/mem-files-api.d.ts +0 -13
  23. package/dist/impl/mem-files-api.d.ts.map +0 -1
  24. package/dist/impl/node-files-api.d.ts +0 -24
  25. package/dist/impl/node-files-api.d.ts.map +0 -1
  26. package/dist/utils/collect-stream.d.ts +0 -16
  27. package/dist/utils/collect-stream.d.ts.map +0 -1
  28. package/dist/utils/file-info-utils.d.ts +0 -10
  29. package/dist/utils/file-info-utils.d.ts.map +0 -1
  30. package/dist/utils/index.d.ts +0 -8
  31. package/dist/utils/index.d.ts.map +0 -1
  32. package/dist/utils/normalize-path.d.ts +0 -21
  33. package/dist/utils/normalize-path.d.ts.map +0 -1
  34. package/dist/utils/path-utils.d.ts.map +0 -1
  35. package/src/files-api.ts +0 -168
  36. package/src/impl/index.ts +0 -6
  37. package/src/impl/mem-files-api.ts +0 -323
  38. package/src/impl/node-files-api.ts +0 -271
  39. package/src/utils/collect-stream.ts +0 -35
  40. package/src/utils/file-info-utils.ts +0 -15
  41. package/src/utils/index.ts +0 -8
  42. package/src/utils/normalize-path.ts +0 -34
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2022 statewalker
3
+ Copyright (c) 2022-2026 statewalker
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,78 +1,126 @@
1
1
  # @statewalker/webrun-files
2
2
 
3
- The core filesystem API for cross-platform file operations. This package provides a clean, minimal interface for reading, writing, and managing files that works consistently whether you're running in Node.js or need an in-memory implementation for browsers and testing.
3
+ Core types and utilities for cross-platform file operations. This package defines the `FilesApi` interface that all storage backends implement, plus utility functions for common file operations.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
8
  npm install @statewalker/webrun-files
9
- # or
10
- pnpm add @statewalker/webrun-files
11
9
  ```
12
10
 
13
- ## Quick Start
11
+ For actual filesystem implementations, install one of these packages:
12
+ - `@statewalker/webrun-files-mem` - In-memory storage
13
+ - `@statewalker/webrun-files-node` - Node.js filesystem
14
+ - `@statewalker/webrun-files-browser` - Browser File System Access API
15
+ - `@statewalker/webrun-files-s3` - AWS S3 / S3-compatible storage
16
+ - `@statewalker/webrun-files-composite` - Mount multiple backends into a unified filesystem
17
+
18
+ ## The FilesApi Interface
19
+
20
+ All implementations provide this interface:
21
+
22
+ ```typescript
23
+ interface FilesApi {
24
+ // Read file content as async iterable of chunks
25
+ read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
26
+
27
+ // Write content to file (creates parent directories)
28
+ write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
29
+
30
+ // Create directory (and parents)
31
+ mkdir(path: string): Promise<void>;
32
+
33
+ // List directory contents
34
+ list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
14
35
 
15
- The fastest way to get going is to create a `FilesApi` instance with one of the built-in backends. Here's an example using the in-memory implementation:
36
+ // Get file/directory metadata
37
+ stats(path: string): Promise<FileStats | undefined>;
38
+
39
+ // Check if path exists
40
+ exists(path: string): Promise<boolean>;
41
+
42
+ // Remove file or directory (recursively)
43
+ remove(path: string): Promise<boolean>;
44
+
45
+ // Move/rename file or directory
46
+ move(source: string, target: string): Promise<boolean>;
47
+
48
+ // Copy file or directory (recursively)
49
+ copy(source: string, target: string): Promise<boolean>;
50
+ }
51
+ ```
52
+
53
+ ## Quick Start
16
54
 
17
55
  ```typescript
18
- import { FilesApi, MemFilesApi } from '@statewalker/webrun-files';
56
+ import { MemFilesApi } from '@statewalker/webrun-files-mem';
57
+ import { readFile, writeText } from '@statewalker/webrun-files';
19
58
 
20
- const files = new FilesApi(new MemFilesApi());
59
+ const files = new MemFilesApi();
21
60
 
22
61
  // Write a file
23
- await files.write('/documents/notes.txt', [
24
- new TextEncoder().encode('Remember to water the plants')
25
- ]);
62
+ await writeText(files, '/documents/notes.txt', 'Remember to water the plants');
26
63
 
27
64
  // Read it back
28
- const content = await files.readFile('/documents/notes.txt');
65
+ const content = await readFile(files, '/documents/notes.txt');
29
66
  console.log(new TextDecoder().decode(content));
30
67
  ```
31
68
 
32
- For Node.js applications that need to work with the actual filesystem, switch to `NodeFilesApi`:
69
+ ## Utility Functions
70
+
71
+ ### Reading Files
33
72
 
34
73
  ```typescript
35
- import { FilesApi, NodeFilesApi } from '@statewalker/webrun-files';
74
+ import { readFile, readText, tryReadFile, tryReadText, readRange, readAt } from '@statewalker/webrun-files';
36
75
 
37
- const files = new FilesApi(new NodeFilesApi('/path/to/root'));
38
- ```
76
+ // Read entire file as Uint8Array
77
+ const content = await readFile(files, '/data.bin');
39
78
 
40
- The `NodeFilesApi` constructor accepts an optional root directory path. When provided, all file operations will be relative to this directory, effectively sandboxing the API.
79
+ // Read as UTF-8 string
80
+ const text = await readText(files, '/config.json');
41
81
 
42
- ## Working with Files
82
+ // Read with undefined return for missing files
83
+ const maybeContent = await tryReadFile(files, '/optional.txt');
84
+ const maybeText = await tryReadText(files, '/optional.txt');
43
85
 
44
- The `FilesApi` class provides everything you need for common file operations. Writing content is straightforward—just pass an array of `Uint8Array` chunks:
86
+ // Read a specific byte range
87
+ const chunk = await readRange(files, '/large.bin', 1000, 500); // 500 bytes starting at position 1000
45
88
 
46
- ```typescript
47
- const text = new TextEncoder().encode('Hello, World!');
48
- await files.write('/greeting.txt', [text]);
89
+ // Read into a buffer at specific offset (like fs.read)
90
+ const buffer = new Uint8Array(100);
91
+ const bytesRead = await readAt(files, '/data.bin', buffer, 0, 100, 500);
49
92
  ```
50
93
 
51
- Reading comes in two flavors. Use `readFile` when you want the entire contents as a single buffer:
94
+ ### Writing Files
52
95
 
53
96
  ```typescript
54
- const content = await files.readFile('/greeting.txt');
55
- ```
97
+ import { writeText } from '@statewalker/webrun-files';
56
98
 
57
- For large files, `read` returns an async generator that yields chunks as they come in, keeping memory usage under control:
99
+ // Write string content as UTF-8
100
+ await writeText(files, '/greeting.txt', 'Hello, World!');
58
101
 
59
- ```typescript
60
- for await (const chunk of files.read('/large-file.bin')) {
61
- process.stdout.write(chunk);
62
- }
102
+ // Write binary content directly
103
+ await files.write('/data.bin', [new Uint8Array([1, 2, 3, 4])]);
104
+
105
+ // Write from multiple chunks
106
+ await files.write('/large.bin', generateChunks());
63
107
  ```
64
108
 
65
- You can also read specific portions of a file by specifying start and end positions:
109
+ ### Path Utilities
66
110
 
67
111
  ```typescript
68
- for await (const chunk of files.read('/data.bin', { start: 100, end: 200 })) {
69
- // Only bytes 100-199
70
- }
112
+ import { normalizePath, joinPath, dirname, basename, extname } from '@statewalker/webrun-files';
113
+
114
+ normalizePath('//foo/./bar//baz/'); // '/foo/bar/baz'
115
+ joinPath('/foo', 'bar', 'baz.txt'); // '/foo/bar/baz.txt'
116
+ dirname('/foo/bar/baz.txt'); // '/foo/bar'
117
+ basename('/foo/bar/baz.txt'); // 'baz.txt'
118
+ extname('/foo/bar/baz.txt'); // '.txt'
71
119
  ```
72
120
 
73
- ## Managing Files and Directories
121
+ ## Working with Files
74
122
 
75
- Checking whether a file or directory exists is simple:
123
+ ### Checking Existence
76
124
 
77
125
  ```typescript
78
126
  if (await files.exists('/config.json')) {
@@ -80,34 +128,54 @@ if (await files.exists('/config.json')) {
80
128
  }
81
129
  ```
82
130
 
83
- To get detailed information about a file, use `stats`:
131
+ ### Getting Metadata
132
+
133
+ `FileStats` is a discriminated union on `kind`. A file always reports a `size`
134
+ and a `lastModified`; a directory reports neither, because a directory has no
135
+ size and a modification time for one is unavailable on stores where a directory
136
+ is only a key prefix. Narrow on `kind` and the fields for that kind are then
137
+ known to be present:
84
138
 
85
139
  ```typescript
86
140
  const info = await files.stats('/photo.jpg');
87
- if (info) {
88
- console.log(`Size: ${info.size} bytes`);
141
+ if (info?.kind === 'file') {
142
+ console.log(`Size: ${info.size} bytes`); // number, never undefined
89
143
  console.log(`Modified: ${new Date(info.lastModified)}`);
90
- console.log(`Type: ${info.kind}`); // "file" or "directory"
144
+ } else if (info?.kind === 'directory') {
145
+ console.log('a directory'); // nothing else to report
91
146
  }
92
147
  ```
93
148
 
94
- Listing directory contents returns an async generator of file info objects:
149
+ A zero-byte file is the file variant with `size: 0`, so test the `kind` rather
150
+ than the truthiness of `size`:
95
151
 
96
152
  ```typescript
97
- for await (const entry of files.list('/documents')) {
98
- console.log(`${entry.name} (${entry.kind})`);
99
- }
153
+ // WRONG - reads an empty file as though it had no size
154
+ if (info.size) { /* ... */ }
155
+
156
+ // RIGHT
157
+ if (info.kind === 'file') { /* info.size may legitimately be 0 */ }
100
158
  ```
101
159
 
102
- Pass `{ recursive: true }` to traverse subdirectories:
160
+ ### Listing Directories
161
+
162
+ `FileInfo` is the same union plus `name` and `path`, so a listing narrows per
163
+ entry exactly as `stats()` does:
103
164
 
104
165
  ```typescript
166
+ // List direct children
167
+ for await (const entry of files.list('/documents')) {
168
+ const size = entry.kind === 'file' ? `${entry.size} bytes` : '';
169
+ console.log(`${entry.name} (${entry.kind}) ${size}`);
170
+ }
171
+
172
+ // List recursively
105
173
  for await (const entry of files.list('/project', { recursive: true })) {
106
174
  console.log(entry.path);
107
175
  }
108
176
  ```
109
177
 
110
- Creating directories, copying, moving, and deleting all work as you'd expect:
178
+ ### File Management
111
179
 
112
180
  ```typescript
113
181
  await files.mkdir('/archive/2024');
@@ -116,83 +184,32 @@ await files.move('/temp/draft.txt', '/documents/final.txt');
116
184
  await files.remove('/old-stuff'); // Recursively deletes directories
117
185
  ```
118
186
 
119
- ## Low-Level File Handle Operations
120
-
121
- When you need fine-grained control over file operations, work directly with file handles. Opening a file gives you a handle that supports reading, writing, and appending at specific positions:
122
-
123
- ```typescript
124
- const handle = await files.open('/data.bin');
125
-
126
- // Get the current file size
127
- console.log(`File is ${handle.size} bytes`);
128
-
129
- // Read a specific range
130
- for await (const chunk of handle.createReadStream({ start: 0, end: 100 })) {
131
- // First 100 bytes
132
- }
133
-
134
- // Write at a specific position
135
- await handle.createWriteStream([newData], { start: 50 });
136
-
137
- // Append to the end
138
- await handle.appendFile([moreData]);
139
-
140
- // Always close when done
141
- await handle.close();
142
- ```
143
-
144
- ## Building Your Own Backend
145
-
146
- The `IFilesApi` interface defines the contract that all backends must implement. At minimum, you need four methods:
147
-
148
- ```typescript
149
- interface IFilesApi {
150
- list(file: FileRef, options?: ListOptions): AsyncGenerator<FileInfo>;
151
- stats(file: FileRef): Promise<FileInfo | undefined>;
152
- remove(file: FileRef): Promise<boolean>;
153
- open(file: FileRef): Promise<FileHandle>;
154
- }
155
- ```
156
-
157
- The `FilesApi` wrapper class builds convenience methods like `read`, `write`, `copy`, and `move` on top of these primitives. Backends can optionally implement native versions of `move`, `copy`, and `mkdir` for better performance.
158
-
159
- Take a look at `MemFilesApi` or `NodeFilesApi` in the source code for reference implementations.
160
-
161
187
  ## Type Reference
162
188
 
163
- The package exports all the TypeScript types you'll need:
164
-
165
189
  ```typescript
166
190
  import type {
167
- IFilesApi, // Core interface for backends
168
- FilesApi, // Wrapper class with convenience methods
169
- FileInfo, // Metadata returned by stats() and list()
170
- FileHandle, // Low-level file operations
171
- FileRef, // string | { path: string }
172
- FileKind, // "file" | "directory"
173
- BinaryStream, // AsyncIterable<Uint8Array> | Iterable<Uint8Array>
174
- ListOptions, // { recursive?: boolean }
175
- CopyOptions, // { recursive?: boolean }
176
- ReadStreamOptions, // { start?: number, end?: number, signal?: AbortSignal }
177
- WriteStreamOptions, // { start?: number, signal?: AbortSignal }
191
+ FilesApi, // Core interface for backends
192
+ FileStats, // Metadata from stats(): FileEntryStats | DirectoryEntryStats
193
+ FileEntryStats, // { kind: "file", size: number, lastModified: number }
194
+ DirectoryEntryStats, // { kind: "directory" }
195
+ FileInfo, // Metadata from list(): FileEntryInfo | DirectoryEntryInfo
196
+ FileEntryInfo, // FileEntryStats & { name, path }
197
+ DirectoryEntryInfo, // DirectoryEntryStats & { name, path }
198
+ FileEntryLocation, // { name: string, path: string }
199
+ FileKind, // "file" | "directory"
200
+ ReadOptions, // { start?: number, length?: number, signal?: AbortSignal }
201
+ ListOptions, // { recursive?: boolean }
178
202
  } from '@statewalker/webrun-files';
179
203
  ```
180
204
 
181
- ## Utility Functions
182
-
183
- The package includes helpful utilities for working with paths and streams:
205
+ ### Implementing a backend
184
206
 
185
- ```typescript
186
- import {
187
- normalizePath, // Clean up path strings
188
- resolveFileRef, // Convert FileRef to normalized path
189
- joinPath, // Join path segments
190
- dirname, // Get directory portion of path
191
- basename, // Get filename portion
192
- extname, // Get file extension
193
- collectGenerator, // Collect async iterable into array
194
- } from '@statewalker/webrun-files';
195
- ```
207
+ An implementation must return **exactly** one variant: a file with both numbers
208
+ present, a directory with nothing but its `kind`. An implementation that knows a
209
+ directory's modification time drops it rather than offering a value that would
210
+ be present on one backend and missing on the next. The parametrized suite in
211
+ `@statewalker/webrun-files-tests` checks this at runtime, and
212
+ `createFilesApiTests` runs it for you.
196
213
 
197
214
  ## License
198
215