@statewalker/webrun-files 0.3.1 → 0.4.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 (51) hide show
  1. package/dist/cjs/index.cjs +564 -0
  2. package/dist/esm/index.js +551 -0
  3. package/dist/files-api.d.ts +44 -0
  4. package/dist/files-api.d.ts.map +1 -0
  5. package/dist/impl/index.d.ts +6 -0
  6. package/dist/impl/index.d.ts.map +1 -0
  7. package/dist/impl/mem-files-api.d.ts +13 -0
  8. package/dist/impl/mem-files-api.d.ts.map +1 -0
  9. package/dist/impl/node-files-api.d.ts +24 -0
  10. package/dist/impl/node-files-api.d.ts.map +1 -0
  11. package/dist/index.d.ts +11 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/types.d.ts +57 -0
  14. package/dist/types.d.ts.map +1 -0
  15. package/dist/utils/collect-stream.d.ts +16 -0
  16. package/dist/utils/collect-stream.d.ts.map +1 -0
  17. package/dist/utils/index.d.ts +7 -0
  18. package/dist/utils/index.d.ts.map +1 -0
  19. package/dist/utils/normalize-path.d.ts +21 -0
  20. package/dist/utils/normalize-path.d.ts.map +1 -0
  21. package/dist/utils/path-utils.d.ts +20 -0
  22. package/dist/utils/path-utils.d.ts.map +1 -0
  23. package/package.json +29 -31
  24. package/src/files-api.ts +167 -0
  25. package/src/impl/index.ts +6 -0
  26. package/src/impl/mem-files-api.ts +306 -0
  27. package/src/impl/node-files-api.ts +261 -0
  28. package/src/index.ts +35 -0
  29. package/src/types.ts +68 -0
  30. package/src/utils/collect-stream.ts +35 -0
  31. package/src/utils/index.ts +7 -0
  32. package/src/utils/normalize-path.ts +34 -0
  33. package/src/utils/path-utils.ts +46 -0
  34. package/CHANGELOG.md +0 -2
  35. package/README.md +0 -17
  36. package/dist/index-umd.js +0 -1054
  37. package/dist/index-umd.min.js +0 -2
  38. package/dist/index.js +0 -1033
  39. package/dist/index.min.js +0 -2
  40. package/index.js +0 -1
  41. package/src/BrowserFilesApi.js +0 -335
  42. package/src/FilesApi.js +0 -133
  43. package/src/MemFilesApi.js +0 -129
  44. package/src/NodeFilesApi.js +0 -222
  45. package/src/addFilesApiLogger.js +0 -25
  46. package/src/getMimeType.js +0 -19
  47. package/src/index.js +0 -10
  48. package/src/mimeTypes.js +0 -96
  49. package/src/normalizePath.js +0 -8
  50. package/src/notImplemented.js +0 -3
  51. package/src/openBrowserFilesApi.js +0 -67
@@ -0,0 +1,564 @@
1
+
2
+ //#region src/utils/collect-stream.ts
3
+ /**
4
+ * Stream collection utilities
5
+ */
6
+ /**
7
+ * Collects all values from an async generator into an array.
8
+ */
9
+ async function collectGenerator(generator) {
10
+ const results = [];
11
+ for await (const item of generator) results.push(item);
12
+ return results;
13
+ }
14
+ /**
15
+ * Converts a synchronous iterable to an async iterable.
16
+ */
17
+ async function* toAsyncIterable(iterable) {
18
+ for (const item of iterable) yield item;
19
+ }
20
+ /**
21
+ * Normalizes BinaryStream to AsyncIterable<Uint8Array>.
22
+ */
23
+ function toBinaryAsyncIterable(data) {
24
+ if (Symbol.asyncIterator in data) return data;
25
+ return toAsyncIterable(data);
26
+ }
27
+
28
+ //#endregion
29
+ //#region src/utils/normalize-path.ts
30
+ /**
31
+ * Normalizes a file path to a consistent format.
32
+ * - Adds leading slash if missing
33
+ * - Removes trailing slash
34
+ * - Removes `.` segments
35
+ * - Collapses multiple slashes
36
+ */
37
+ function normalizePath(filePath) {
38
+ const segments = filePath.split("/").filter((s) => !!s && s !== ".");
39
+ if (segments.length === 0) return "/";
40
+ return `/${segments.join("/")}`;
41
+ }
42
+ /**
43
+ * Extracts path string from FileRef.
44
+ */
45
+ function toPath(file) {
46
+ return typeof file === "object" ? file.path : String(file);
47
+ }
48
+ /**
49
+ * Normalizes FileRef to a path string.
50
+ */
51
+ function resolveFileRef(file) {
52
+ return normalizePath(toPath(file));
53
+ }
54
+
55
+ //#endregion
56
+ //#region src/utils/path-utils.ts
57
+ /**
58
+ * Path manipulation utilities
59
+ */
60
+ /**
61
+ * Joins path segments with proper normalization.
62
+ */
63
+ function joinPath(...segments) {
64
+ return normalizePath(segments.join("/"));
65
+ }
66
+ /**
67
+ * Returns the directory portion of a path.
68
+ */
69
+ function dirname(path) {
70
+ const normalized = normalizePath(path);
71
+ const lastSlash = normalized.lastIndexOf("/");
72
+ if (lastSlash <= 0) return "/";
73
+ return normalized.substring(0, lastSlash);
74
+ }
75
+ /**
76
+ * Returns the filename portion of a path.
77
+ */
78
+ function basename(path, ext) {
79
+ const normalized = normalizePath(path);
80
+ const lastSlash = normalized.lastIndexOf("/");
81
+ const name = lastSlash === -1 ? normalized : normalized.substring(lastSlash + 1);
82
+ if (ext && name.endsWith(ext)) return name.substring(0, name.length - ext.length);
83
+ return name;
84
+ }
85
+ /**
86
+ * Returns the file extension including the dot.
87
+ */
88
+ function extname(path) {
89
+ const name = basename(path);
90
+ const dotIndex = name.lastIndexOf(".");
91
+ if (dotIndex <= 0) return "";
92
+ return name.substring(dotIndex);
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/files-api.ts
97
+ const IGNORE_FILE = ".gitkeep";
98
+ var FilesApi = class {
99
+ constructor(fs) {
100
+ this.fs = fs;
101
+ }
102
+ list(file, options) {
103
+ return this.fs.list(file, options);
104
+ }
105
+ stats(file) {
106
+ return this.fs.stats(file);
107
+ }
108
+ remove(file) {
109
+ return this.fs.remove(file);
110
+ }
111
+ open(file) {
112
+ return this.fs.open(file);
113
+ }
114
+ /**
115
+ * Checks if a file or directory exists.
116
+ */
117
+ async exists(file) {
118
+ return await this.stats(file) !== void 0;
119
+ }
120
+ /**
121
+ * Reads file content as a stream.
122
+ */
123
+ async *read(file, options = {}) {
124
+ const handle = await this.open(file);
125
+ try {
126
+ yield* handle.createReadStream(options);
127
+ } finally {
128
+ await handle.close();
129
+ }
130
+ }
131
+ /**
132
+ * Writes content to a file.
133
+ */
134
+ async write(file, content) {
135
+ const handle = await this.open(file);
136
+ try {
137
+ await handle.createWriteStream(toBinaryAsyncIterable(content));
138
+ } finally {
139
+ await handle.close();
140
+ }
141
+ }
142
+ /**
143
+ * Reads entire file into a single buffer.
144
+ */
145
+ async readFile(file) {
146
+ const chunks = [];
147
+ let totalLength = 0;
148
+ for await (const chunk of this.read(file)) {
149
+ chunks.push(chunk);
150
+ totalLength += chunk.length;
151
+ }
152
+ if (chunks.length === 0) return new Uint8Array(0);
153
+ if (chunks.length === 1) return chunks[0];
154
+ const result = new Uint8Array(totalLength);
155
+ let offset = 0;
156
+ for (const chunk of chunks) {
157
+ result.set(chunk, offset);
158
+ offset += chunk.length;
159
+ }
160
+ return result;
161
+ }
162
+ /**
163
+ * Copies file or directory.
164
+ * Uses native implementation if available, otherwise falls back to read/write.
165
+ */
166
+ async copy(source, target, options = {}) {
167
+ if (this.fs.copy) return this.fs.copy(source, target, options);
168
+ const sourceInfo = await this.stats(source);
169
+ if (!sourceInfo) return false;
170
+ const sourcePath = resolveFileRef(source);
171
+ const targetPath = resolveFileRef(target);
172
+ const recursive = options.recursive ?? true;
173
+ if (sourceInfo.kind === "directory") for await (const entry of this.list(source, { recursive })) {
174
+ if (entry.kind === "directory") continue;
175
+ const newTargetPath = targetPath + entry.path.substring(sourcePath.length);
176
+ await this.write(newTargetPath, this.read(entry.path));
177
+ }
178
+ else await this.write(targetPath, this.read(sourcePath));
179
+ return true;
180
+ }
181
+ /**
182
+ * Moves file or directory.
183
+ * Uses native implementation if available, otherwise falls back to copy+delete.
184
+ */
185
+ async move(source, target) {
186
+ if (this.fs.move) return this.fs.move(source, target);
187
+ if (!await this.copy(source, target)) return false;
188
+ await this.remove(source);
189
+ return true;
190
+ }
191
+ /**
192
+ * Creates a directory.
193
+ * Uses native implementation if available, otherwise creates a placeholder file.
194
+ */
195
+ async mkdir(file) {
196
+ if (this.fs.mkdir) return this.fs.mkdir(file);
197
+ const placeholderPath = joinPath(resolveFileRef(file), IGNORE_FILE);
198
+ await this.write(placeholderPath, [new Uint8Array(0)]);
199
+ }
200
+ };
201
+
202
+ //#endregion
203
+ //#region src/impl/mem-files-api.ts
204
+ var MemFileHandle = class {
205
+ constructor(store, path) {
206
+ this.store = store;
207
+ this.path = path;
208
+ }
209
+ get size() {
210
+ const entry = this.store.get(this.path);
211
+ if (entry?.kind === "file") return entry.content.length;
212
+ return 0;
213
+ }
214
+ getOrCreateFile() {
215
+ let entry = this.store.get(this.path);
216
+ if (!entry || entry.kind !== "file") {
217
+ entry = {
218
+ kind: "file",
219
+ content: new Uint8Array(0),
220
+ lastModified: Date.now()
221
+ };
222
+ this.store.set(this.path, entry);
223
+ }
224
+ return entry;
225
+ }
226
+ async close() {}
227
+ async appendFile(data, options = {}) {
228
+ const file = this.getOrCreateFile();
229
+ const chunks = [file.content];
230
+ let bytesWritten = 0;
231
+ for await (const chunk of toBinaryAsyncIterable(data)) {
232
+ if (options.signal?.aborted) throw new Error("Operation aborted");
233
+ chunks.push(chunk);
234
+ bytesWritten += chunk.length;
235
+ }
236
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
237
+ const merged = new Uint8Array(totalLength);
238
+ let offset = 0;
239
+ for (const chunk of chunks) {
240
+ merged.set(chunk, offset);
241
+ offset += chunk.length;
242
+ }
243
+ file.content = merged;
244
+ file.lastModified = Date.now();
245
+ return bytesWritten;
246
+ }
247
+ async *createReadStream(options = {}) {
248
+ const entry = this.store.get(this.path);
249
+ if (!entry || entry.kind !== "file") return;
250
+ const { start = 0, end = Infinity, signal } = options;
251
+ const bufferSize = 8192;
252
+ const content = entry.content;
253
+ const actualEnd = Math.min(end, content.length);
254
+ let position = start;
255
+ while (position < actualEnd) {
256
+ if (signal?.aborted) throw new Error("Operation aborted");
257
+ const remaining = actualEnd - position;
258
+ const toRead = Math.min(bufferSize, remaining);
259
+ yield content.subarray(position, position + toRead);
260
+ position += toRead;
261
+ }
262
+ }
263
+ async createWriteStream(data, options = {}) {
264
+ const { start = 0, signal } = options;
265
+ const file = this.getOrCreateFile();
266
+ const chunks = [];
267
+ let bytesWritten = 0;
268
+ if (start > 0 && file.content.length > 0) chunks.push(file.content.subarray(0, Math.min(start, file.content.length)));
269
+ if (start > file.content.length) chunks.push(new Uint8Array(start - file.content.length));
270
+ for await (const chunk of toBinaryAsyncIterable(data)) {
271
+ if (signal?.aborted) throw new Error("Operation aborted");
272
+ chunks.push(chunk);
273
+ bytesWritten += chunk.length;
274
+ }
275
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
276
+ const merged = new Uint8Array(totalLength);
277
+ let offset = 0;
278
+ for (const chunk of chunks) {
279
+ merged.set(chunk, offset);
280
+ offset += chunk.length;
281
+ }
282
+ file.content = merged;
283
+ file.lastModified = Date.now();
284
+ return bytesWritten;
285
+ }
286
+ };
287
+ var MemFilesApi = class {
288
+ store = /* @__PURE__ */ new Map();
289
+ async *list(file, options = {}) {
290
+ const normalized = resolveFileRef(file);
291
+ const prefix = normalized === "/" ? "/" : `${normalized}/`;
292
+ const { recursive = false } = options;
293
+ const seen = /* @__PURE__ */ new Set();
294
+ for (const [path, entry] of this.store.entries()) {
295
+ if (!path.startsWith(prefix)) continue;
296
+ const segments = path.substring(prefix.length).split("/");
297
+ if (segments.length === 0 || segments[0] === "") continue;
298
+ if (segments.length === 1) {
299
+ if (seen.has(path)) continue;
300
+ seen.add(path);
301
+ const info = {
302
+ kind: entry.kind,
303
+ name: basename(path),
304
+ path,
305
+ lastModified: entry.lastModified
306
+ };
307
+ if (entry.kind === "file") info.size = entry.content.length;
308
+ yield info;
309
+ } else if (recursive) {
310
+ if (seen.has(path)) continue;
311
+ seen.add(path);
312
+ const info = {
313
+ kind: entry.kind,
314
+ name: basename(path),
315
+ path,
316
+ lastModified: entry.lastModified
317
+ };
318
+ if (entry.kind === "file") info.size = entry.content.length;
319
+ yield info;
320
+ } else {
321
+ const dirName = segments[0];
322
+ const dirPath = joinPath(normalized, dirName);
323
+ if (!seen.has(dirPath)) {
324
+ seen.add(dirPath);
325
+ yield {
326
+ kind: "directory",
327
+ name: dirName,
328
+ path: dirPath,
329
+ lastModified: 0
330
+ };
331
+ }
332
+ }
333
+ }
334
+ }
335
+ async stats(file) {
336
+ const normalized = resolveFileRef(file);
337
+ const entry = this.store.get(normalized);
338
+ if (entry) {
339
+ const info = {
340
+ kind: entry.kind,
341
+ name: basename(normalized),
342
+ path: normalized,
343
+ lastModified: entry.lastModified
344
+ };
345
+ if (entry.kind === "file") info.size = entry.content.length;
346
+ return info;
347
+ }
348
+ if (normalized === "/") return {
349
+ kind: "directory",
350
+ name: "",
351
+ path: "/",
352
+ lastModified: 0
353
+ };
354
+ const prefix = `${normalized}/`;
355
+ for (const path of this.store.keys()) if (path.startsWith(prefix)) return {
356
+ kind: "directory",
357
+ name: basename(normalized),
358
+ path: normalized,
359
+ lastModified: 0
360
+ };
361
+ }
362
+ async remove(file) {
363
+ const normalized = resolveFileRef(file);
364
+ const prefix = `${normalized}/`;
365
+ let removed = false;
366
+ if (this.store.delete(normalized)) removed = true;
367
+ for (const path of [...this.store.keys()]) if (path.startsWith(prefix)) {
368
+ this.store.delete(path);
369
+ removed = true;
370
+ }
371
+ return removed;
372
+ }
373
+ async open(file) {
374
+ const normalized = resolveFileRef(file);
375
+ return new MemFileHandle(this.store, normalized);
376
+ }
377
+ async mkdir(file) {
378
+ const normalized = resolveFileRef(file);
379
+ if (!this.store.has(normalized)) this.store.set(normalized, {
380
+ kind: "directory",
381
+ lastModified: Date.now()
382
+ });
383
+ }
384
+ };
385
+
386
+ //#endregion
387
+ //#region src/impl/node-files-api.ts
388
+ /**
389
+ * Wraps Node.js FileHandle to implement our FileHandle interface.
390
+ */
391
+ var NodeFileHandleWrapper = class {
392
+ constructor(handle, _size) {
393
+ this.handle = handle;
394
+ this._size = _size;
395
+ }
396
+ get size() {
397
+ return this._size;
398
+ }
399
+ async close() {
400
+ await this.handle.close();
401
+ }
402
+ async appendFile(data, options = {}) {
403
+ let bytesWritten = 0;
404
+ const asyncData = toBinaryAsyncIterable(data);
405
+ for await (const chunk of asyncData) {
406
+ if (options.signal?.aborted) throw new Error("Operation aborted");
407
+ await this.handle.appendFile(chunk);
408
+ bytesWritten += chunk.length;
409
+ }
410
+ this._size = (await this.handle.stat()).size;
411
+ return bytesWritten;
412
+ }
413
+ async *createReadStream(options = {}) {
414
+ const { start = 0, end = Infinity, signal } = options;
415
+ const bufferSize = 8192;
416
+ let position = start;
417
+ const actualEnd = Math.min(end, this._size);
418
+ while (position < actualEnd) {
419
+ if (signal?.aborted) throw new Error("Operation aborted");
420
+ const remaining = actualEnd - position;
421
+ const toRead = Math.min(bufferSize, remaining);
422
+ const buffer = new Uint8Array(toRead);
423
+ const { bytesRead } = await this.handle.read(buffer, 0, toRead, position);
424
+ if (bytesRead === 0) break;
425
+ yield buffer.subarray(0, bytesRead);
426
+ position += bytesRead;
427
+ }
428
+ }
429
+ async createWriteStream(data, options = {}) {
430
+ const { start = 0, signal } = options;
431
+ let position = start;
432
+ let bytesWritten = 0;
433
+ const asyncData = toBinaryAsyncIterable(data);
434
+ await this.handle.truncate(start);
435
+ for await (const chunk of asyncData) {
436
+ if (signal?.aborted) throw new Error("Operation aborted");
437
+ const { bytesWritten: written } = await this.handle.write(chunk, 0, chunk.length, position);
438
+ position += written;
439
+ bytesWritten += written;
440
+ }
441
+ this._size = (await this.handle.stat()).size;
442
+ return bytesWritten;
443
+ }
444
+ async chmod(mode) {
445
+ await this.handle.chmod(mode);
446
+ }
447
+ async chown(uid, gid) {
448
+ await this.handle.chown(uid, gid);
449
+ }
450
+ };
451
+ var NodeFilesApi = class {
452
+ fs;
453
+ rootDir;
454
+ constructor(options) {
455
+ this.fs = options.fs;
456
+ this.rootDir = options.rootDir ?? "";
457
+ }
458
+ resolvePath(file) {
459
+ const normalized = resolveFileRef(file);
460
+ return this.rootDir + normalized;
461
+ }
462
+ async *list(file, options = {}) {
463
+ const dirPath = this.resolvePath(file);
464
+ const { recursive = false } = options;
465
+ try {
466
+ const entries = await this.fs.readdir(dirPath, { withFileTypes: true });
467
+ for (const entry of entries) {
468
+ const entryPath = joinPath(resolveFileRef(file), entry.name);
469
+ const fullPath = this.rootDir + entryPath;
470
+ const info = {
471
+ kind: entry.isDirectory() ? "directory" : "file",
472
+ name: entry.name,
473
+ path: entryPath,
474
+ lastModified: 0
475
+ };
476
+ try {
477
+ const stat = await this.fs.stat(fullPath);
478
+ info.size = stat.size;
479
+ info.lastModified = stat.mtimeMs;
480
+ } catch {}
481
+ yield info;
482
+ if (recursive && entry.isDirectory()) yield* this.list(entryPath, options);
483
+ }
484
+ } catch {}
485
+ }
486
+ async stats(file) {
487
+ const fullPath = this.resolvePath(file);
488
+ const normalized = resolveFileRef(file);
489
+ try {
490
+ const stat = await this.fs.stat(fullPath);
491
+ return {
492
+ kind: stat.isDirectory() ? "directory" : "file",
493
+ name: basename(normalized),
494
+ path: normalized,
495
+ size: stat.size,
496
+ lastModified: stat.mtimeMs
497
+ };
498
+ } catch {
499
+ return;
500
+ }
501
+ }
502
+ async remove(file) {
503
+ const fullPath = this.resolvePath(file);
504
+ try {
505
+ if ((await this.fs.stat(fullPath)).isDirectory()) await this.fs.rm(fullPath, { recursive: true });
506
+ else await this.fs.unlink(fullPath);
507
+ return true;
508
+ } catch {
509
+ return false;
510
+ }
511
+ }
512
+ async open(file) {
513
+ const fullPath = this.resolvePath(file);
514
+ const normalized = resolveFileRef(file);
515
+ const dir = this.rootDir + dirname(normalized);
516
+ await this.fs.mkdir(dir, { recursive: true });
517
+ const handle = await this.fs.open(fullPath, "a+");
518
+ return new NodeFileHandleWrapper(handle, (await handle.stat()).size);
519
+ }
520
+ async move(source, target) {
521
+ const sourcePath = this.resolvePath(source);
522
+ const targetPath = this.resolvePath(target);
523
+ try {
524
+ const targetDir = dirname(targetPath);
525
+ await this.fs.mkdir(targetDir, { recursive: true });
526
+ await this.fs.rename(sourcePath, targetPath);
527
+ return true;
528
+ } catch {
529
+ return false;
530
+ }
531
+ }
532
+ async copy(source, target, options = {}) {
533
+ const sourcePath = this.resolvePath(source);
534
+ const targetPath = this.resolvePath(target);
535
+ const { recursive = true } = options;
536
+ try {
537
+ const targetDir = dirname(targetPath);
538
+ await this.fs.mkdir(targetDir, { recursive: true });
539
+ await this.fs.cp(sourcePath, targetPath, { recursive });
540
+ return true;
541
+ } catch {
542
+ return false;
543
+ }
544
+ }
545
+ async mkdir(file) {
546
+ const fullPath = this.resolvePath(file);
547
+ await this.fs.mkdir(fullPath, { recursive: true });
548
+ }
549
+ };
550
+
551
+ //#endregion
552
+ exports.FilesApi = FilesApi;
553
+ exports.MemFilesApi = MemFilesApi;
554
+ exports.NodeFilesApi = NodeFilesApi;
555
+ exports.basename = basename;
556
+ exports.collectGenerator = collectGenerator;
557
+ exports.dirname = dirname;
558
+ exports.extname = extname;
559
+ exports.joinPath = joinPath;
560
+ exports.normalizePath = normalizePath;
561
+ exports.resolveFileRef = resolveFileRef;
562
+ exports.toAsyncIterable = toAsyncIterable;
563
+ exports.toBinaryAsyncIterable = toBinaryAsyncIterable;
564
+ exports.toPath = toPath;