@scelar/nodepod 1.0.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 (134) hide show
  1. package/LICENSE +43 -0
  2. package/README.md +240 -0
  3. package/dist/child_process-BJOMsZje.js +8233 -0
  4. package/dist/child_process-BJOMsZje.js.map +1 -0
  5. package/dist/child_process-Cj8vOcuc.cjs +7434 -0
  6. package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
  7. package/dist/index-Cb1Cgdnd.js +35308 -0
  8. package/dist/index-Cb1Cgdnd.js.map +1 -0
  9. package/dist/index-DsMGS-xc.cjs +37195 -0
  10. package/dist/index-DsMGS-xc.cjs.map +1 -0
  11. package/dist/index.cjs +65 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +59 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +95 -0
  16. package/src/__tests__/smoke.test.ts +11 -0
  17. package/src/constants/cdn-urls.ts +18 -0
  18. package/src/constants/config.ts +236 -0
  19. package/src/cross-origin.ts +26 -0
  20. package/src/engine-factory.ts +176 -0
  21. package/src/engine-types.ts +56 -0
  22. package/src/helpers/byte-encoding.ts +39 -0
  23. package/src/helpers/digest.ts +9 -0
  24. package/src/helpers/event-loop.ts +96 -0
  25. package/src/helpers/wasm-cache.ts +133 -0
  26. package/src/iframe-sandbox.ts +141 -0
  27. package/src/index.ts +192 -0
  28. package/src/isolation-helpers.ts +148 -0
  29. package/src/memory-volume.ts +941 -0
  30. package/src/module-transformer.ts +368 -0
  31. package/src/packages/archive-extractor.ts +248 -0
  32. package/src/packages/browser-bundler.ts +284 -0
  33. package/src/packages/installer.ts +396 -0
  34. package/src/packages/registry-client.ts +131 -0
  35. package/src/packages/version-resolver.ts +411 -0
  36. package/src/polyfills/assert.ts +384 -0
  37. package/src/polyfills/async_hooks.ts +144 -0
  38. package/src/polyfills/buffer.ts +628 -0
  39. package/src/polyfills/child_process.ts +2288 -0
  40. package/src/polyfills/chokidar.ts +336 -0
  41. package/src/polyfills/cluster.ts +106 -0
  42. package/src/polyfills/console.ts +136 -0
  43. package/src/polyfills/constants.ts +123 -0
  44. package/src/polyfills/crypto.ts +885 -0
  45. package/src/polyfills/dgram.ts +87 -0
  46. package/src/polyfills/diagnostics_channel.ts +76 -0
  47. package/src/polyfills/dns.ts +134 -0
  48. package/src/polyfills/domain.ts +68 -0
  49. package/src/polyfills/esbuild.ts +854 -0
  50. package/src/polyfills/events.ts +276 -0
  51. package/src/polyfills/fs.ts +2888 -0
  52. package/src/polyfills/fsevents.ts +79 -0
  53. package/src/polyfills/http.ts +1449 -0
  54. package/src/polyfills/http2.ts +199 -0
  55. package/src/polyfills/https.ts +76 -0
  56. package/src/polyfills/inspector.ts +62 -0
  57. package/src/polyfills/lightningcss.ts +105 -0
  58. package/src/polyfills/module.ts +191 -0
  59. package/src/polyfills/net.ts +353 -0
  60. package/src/polyfills/os.ts +238 -0
  61. package/src/polyfills/path.ts +206 -0
  62. package/src/polyfills/perf_hooks.ts +102 -0
  63. package/src/polyfills/process.ts +690 -0
  64. package/src/polyfills/punycode.ts +159 -0
  65. package/src/polyfills/querystring.ts +93 -0
  66. package/src/polyfills/quic.ts +118 -0
  67. package/src/polyfills/readdirp.ts +229 -0
  68. package/src/polyfills/readline.ts +692 -0
  69. package/src/polyfills/repl.ts +134 -0
  70. package/src/polyfills/rollup.ts +119 -0
  71. package/src/polyfills/sea.ts +33 -0
  72. package/src/polyfills/sqlite.ts +78 -0
  73. package/src/polyfills/stream.ts +1620 -0
  74. package/src/polyfills/string_decoder.ts +25 -0
  75. package/src/polyfills/tailwindcss-oxide.ts +309 -0
  76. package/src/polyfills/test.ts +197 -0
  77. package/src/polyfills/timers.ts +32 -0
  78. package/src/polyfills/tls.ts +105 -0
  79. package/src/polyfills/trace_events.ts +50 -0
  80. package/src/polyfills/tty.ts +71 -0
  81. package/src/polyfills/url.ts +174 -0
  82. package/src/polyfills/util.ts +559 -0
  83. package/src/polyfills/v8.ts +126 -0
  84. package/src/polyfills/vm.ts +132 -0
  85. package/src/polyfills/volume-registry.ts +15 -0
  86. package/src/polyfills/wasi.ts +44 -0
  87. package/src/polyfills/worker_threads.ts +326 -0
  88. package/src/polyfills/ws.ts +595 -0
  89. package/src/polyfills/zlib.ts +881 -0
  90. package/src/request-proxy.ts +716 -0
  91. package/src/script-engine.ts +3375 -0
  92. package/src/sdk/nodepod-fs.ts +93 -0
  93. package/src/sdk/nodepod-process.ts +86 -0
  94. package/src/sdk/nodepod-terminal.ts +350 -0
  95. package/src/sdk/nodepod.ts +509 -0
  96. package/src/sdk/types.ts +70 -0
  97. package/src/shell/commands/bun.ts +121 -0
  98. package/src/shell/commands/directory.ts +297 -0
  99. package/src/shell/commands/file-ops.ts +525 -0
  100. package/src/shell/commands/git.ts +2142 -0
  101. package/src/shell/commands/node.ts +80 -0
  102. package/src/shell/commands/npm.ts +198 -0
  103. package/src/shell/commands/pm-types.ts +45 -0
  104. package/src/shell/commands/pnpm.ts +82 -0
  105. package/src/shell/commands/search.ts +264 -0
  106. package/src/shell/commands/shell-env.ts +352 -0
  107. package/src/shell/commands/text-processing.ts +1152 -0
  108. package/src/shell/commands/yarn.ts +84 -0
  109. package/src/shell/shell-builtins.ts +19 -0
  110. package/src/shell/shell-helpers.ts +250 -0
  111. package/src/shell/shell-interpreter.ts +514 -0
  112. package/src/shell/shell-parser.ts +429 -0
  113. package/src/shell/shell-types.ts +85 -0
  114. package/src/syntax-transforms.ts +561 -0
  115. package/src/threading/engine-worker.ts +64 -0
  116. package/src/threading/inline-worker.ts +372 -0
  117. package/src/threading/offload-types.ts +112 -0
  118. package/src/threading/offload-worker.ts +383 -0
  119. package/src/threading/offload.ts +271 -0
  120. package/src/threading/process-context.ts +92 -0
  121. package/src/threading/process-handle.ts +275 -0
  122. package/src/threading/process-manager.ts +956 -0
  123. package/src/threading/process-worker-entry.ts +854 -0
  124. package/src/threading/shared-vfs.ts +352 -0
  125. package/src/threading/sync-channel.ts +135 -0
  126. package/src/threading/task-queue.ts +177 -0
  127. package/src/threading/vfs-bridge.ts +231 -0
  128. package/src/threading/worker-pool.ts +233 -0
  129. package/src/threading/worker-protocol.ts +358 -0
  130. package/src/threading/worker-vfs.ts +218 -0
  131. package/src/types/externals.d.ts +38 -0
  132. package/src/types/fs-streams.ts +142 -0
  133. package/src/types/manifest.ts +17 -0
  134. package/src/worker-sandbox.ts +90 -0
@@ -0,0 +1,941 @@
1
+ // in-memory VFS with POSIX-like semantics
2
+
3
+ import type { VolumeSnapshot, VolumeEntry } from './engine-types';
4
+ import { bytesToBase64, base64ToBytes } from './helpers/byte-encoding';
5
+ import { MOCK_IDS, MOCK_FS } from './constants/config';
6
+
7
+ export interface VolumeNode {
8
+ kind: 'file' | 'directory' | 'symlink';
9
+ content?: Uint8Array;
10
+ children?: Map<string, VolumeNode>;
11
+ target?: string;
12
+ modified: number;
13
+ }
14
+
15
+ type FileChangeHandler = (filePath: string, content: string) => void;
16
+ type FileDeleteHandler = (filePath: string) => void;
17
+ type VolumeEventHandler = FileChangeHandler | FileDeleteHandler;
18
+
19
+ export interface FileStat {
20
+ isFile(): boolean;
21
+ isDirectory(): boolean;
22
+ isSymbolicLink(): boolean;
23
+ isBlockDevice(): boolean;
24
+ isCharacterDevice(): boolean;
25
+ isFIFO(): boolean;
26
+ isSocket(): boolean;
27
+ size: number;
28
+ mode: number;
29
+ mtime: Date;
30
+ atime: Date;
31
+ ctime: Date;
32
+ birthtime: Date;
33
+ mtimeMs: number;
34
+ atimeMs: number;
35
+ ctimeMs: number;
36
+ birthtimeMs: number;
37
+ nlink: number;
38
+ uid: number;
39
+ gid: number;
40
+ dev: number;
41
+ ino: number;
42
+ rdev: number;
43
+ blksize: number;
44
+ blocks: number;
45
+ atimeNs: bigint;
46
+ mtimeNs: bigint;
47
+ ctimeNs: bigint;
48
+ birthtimeNs: bigint;
49
+ }
50
+
51
+ export type WatchEventKind = 'change' | 'rename';
52
+ export type WatchCallback = (event: WatchEventKind, name: string | null) => void;
53
+
54
+ export interface FileWatchHandle {
55
+ close(): void;
56
+ ref(): this;
57
+ unref(): this;
58
+ on(event: string, listener: (...args: unknown[]) => void): this;
59
+ once(event: string, listener: (...args: unknown[]) => void): this;
60
+ removeListener(event: string, listener: (...args: unknown[]) => void): this;
61
+ off(event: string, listener: (...args: unknown[]) => void): this;
62
+ addListener(event: string, listener: (...args: unknown[]) => void): this;
63
+ removeAllListeners(event?: string): this;
64
+ emit(event: string, ...args: unknown[]): boolean;
65
+ }
66
+
67
+ // minimal EventEmitter-based FSWatcher for fs.watch()
68
+ class FSWatcher implements FileWatchHandle {
69
+ private _listeners = new Map<string, Array<(...args: unknown[]) => void>>();
70
+ private _closeFn: (() => void) | null = null;
71
+
72
+ constructor(closeFn: () => void) {
73
+ this._closeFn = closeFn;
74
+ }
75
+
76
+ close(): void {
77
+ if (this._closeFn) { this._closeFn(); this._closeFn = null; }
78
+ this._listeners.clear();
79
+ }
80
+ ref(): this { return this; }
81
+ unref(): this { return this; }
82
+
83
+ on(event: string, listener: (...args: unknown[]) => void): this {
84
+ if (!this._listeners.has(event)) this._listeners.set(event, []);
85
+ this._listeners.get(event)!.push(listener);
86
+ return this;
87
+ }
88
+ addListener(event: string, listener: (...args: unknown[]) => void): this {
89
+ return this.on(event, listener);
90
+ }
91
+ once(event: string, listener: (...args: unknown[]) => void): this {
92
+ const wrapped = (...args: unknown[]) => {
93
+ this.removeListener(event, wrapped);
94
+ listener(...args);
95
+ };
96
+ return this.on(event, wrapped);
97
+ }
98
+ off(event: string, listener: (...args: unknown[]) => void): this {
99
+ return this.removeListener(event, listener);
100
+ }
101
+ removeListener(event: string, listener: (...args: unknown[]) => void): this {
102
+ const list = this._listeners.get(event);
103
+ if (list) {
104
+ const idx = list.indexOf(listener);
105
+ if (idx >= 0) list.splice(idx, 1);
106
+ }
107
+ return this;
108
+ }
109
+ removeAllListeners(event?: string): this {
110
+ if (event) this._listeners.delete(event);
111
+ else this._listeners.clear();
112
+ return this;
113
+ }
114
+ emit(event: string, ...args: unknown[]): boolean {
115
+ const list = this._listeners.get(event);
116
+ if (!list || list.length === 0) return false;
117
+ for (const fn of [...list]) {
118
+ try { fn(...args); } catch (e) { console.error('[FSWatcher] listener error:', e); }
119
+ }
120
+ return true;
121
+ }
122
+ }
123
+
124
+ interface ActiveWatcher {
125
+ callback: WatchCallback;
126
+ recursive: boolean;
127
+ active: boolean;
128
+ }
129
+
130
+ export interface SystemError extends Error {
131
+ code: string;
132
+ errno: number;
133
+ syscall: string;
134
+ path?: string;
135
+ }
136
+
137
+ export function makeSystemError(
138
+ code: 'ENOENT' | 'ENOTDIR' | 'EISDIR' | 'EEXIST' | 'ENOTEMPTY',
139
+ syscall: string,
140
+ targetPath: string,
141
+ detail?: string
142
+ ): SystemError {
143
+ const errnoTable: Record<string, number> = {
144
+ ENOENT: -2,
145
+ ENOTDIR: -20,
146
+ EISDIR: -21,
147
+ EEXIST: -17,
148
+ ENOTEMPTY: -39,
149
+ };
150
+
151
+ const descriptions: Record<string, string> = {
152
+ ENOENT: 'no such file or directory',
153
+ ENOTDIR: 'not a directory',
154
+ EISDIR: 'is a directory',
155
+ EEXIST: 'file already exists',
156
+ ENOTEMPTY: 'directory not empty',
157
+ };
158
+
159
+ const err = new Error(
160
+ detail || `${code}: ${descriptions[code]}, ${syscall} '${targetPath}'`
161
+ ) as SystemError;
162
+ err.code = code;
163
+ err.errno = errnoTable[code];
164
+ err.syscall = syscall;
165
+ err.path = targetPath;
166
+ return err;
167
+ }
168
+
169
+ export class MemoryVolume {
170
+ private tree: VolumeNode;
171
+ private textEncoder = new TextEncoder();
172
+ private textDecoder = new TextDecoder();
173
+ private activeWatchers = new Map<string, Set<ActiveWatcher>>();
174
+ private subscribers = new Map<string, Set<VolumeEventHandler>>();
175
+
176
+ constructor() {
177
+ this.tree = {
178
+ kind: 'directory',
179
+ children: new Map(),
180
+ modified: Date.now(),
181
+ };
182
+ }
183
+
184
+ // ---- Event subscription ----
185
+
186
+ on(event: 'change', handler: FileChangeHandler): this;
187
+ on(event: 'delete', handler: FileDeleteHandler): this;
188
+ on(event: string, handler: VolumeEventHandler): this {
189
+ if (!this.subscribers.has(event)) {
190
+ this.subscribers.set(event, new Set());
191
+ }
192
+ this.subscribers.get(event)!.add(handler);
193
+ return this;
194
+ }
195
+
196
+ off(event: 'change', handler: FileChangeHandler): this;
197
+ off(event: 'delete', handler: FileDeleteHandler): this;
198
+ off(event: string, handler: VolumeEventHandler): this {
199
+ const handlers = this.subscribers.get(event);
200
+ if (handlers) handlers.delete(handler);
201
+ return this;
202
+ }
203
+
204
+ private broadcast(event: 'change', path: string, content: string): void;
205
+ private broadcast(event: 'delete', path: string): void;
206
+ private broadcast(event: string, ...args: unknown[]): void {
207
+ const handlers = this.subscribers.get(event);
208
+ if (handlers) {
209
+ for (const handler of handlers) {
210
+ try {
211
+ (handler as (...a: unknown[]) => void)(...args);
212
+ } catch (e) {
213
+ console.error('Volume event handler error:', e);
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ // ---- Snapshot serialization ----
220
+
221
+ toSnapshot(): VolumeSnapshot {
222
+ const entries: VolumeEntry[] = [];
223
+ this.collectEntries('/', this.tree, entries);
224
+ return { entries };
225
+ }
226
+
227
+ private collectEntries(currentPath: string, node: VolumeNode, result: VolumeEntry[]): void {
228
+ if (node.kind === 'file') {
229
+ let data = '';
230
+ if (node.content && node.content.length > 0) {
231
+ data = bytesToBase64(node.content);
232
+ }
233
+ result.push({ path: currentPath, kind: 'file', data });
234
+ } else if (node.kind === 'symlink') {
235
+ result.push({ path: currentPath, kind: 'file', data: `symlink:${node.target}` });
236
+ } else if (node.kind === 'directory') {
237
+ result.push({ path: currentPath, kind: 'directory' });
238
+ if (node.children) {
239
+ for (const [name, child] of node.children) {
240
+ const childPath = currentPath === '/' ? `/${name}` : `${currentPath}/${name}`;
241
+ this.collectEntries(childPath, child, result);
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ // restore from a binary snapshot (flat ArrayBuffer + offset manifest, used by workers)
248
+ static fromBinarySnapshot(snapshot: { manifest: Array<{ path: string; offset: number; length: number; isDirectory: boolean }>; data: ArrayBuffer }): MemoryVolume {
249
+ const vol = new MemoryVolume();
250
+ const fullData = new Uint8Array(snapshot.data);
251
+
252
+ // directories first, then by depth
253
+ const sorted = [...snapshot.manifest].sort((a, b) => {
254
+ if (a.isDirectory && !b.isDirectory) return -1;
255
+ if (!a.isDirectory && b.isDirectory) return 1;
256
+ return a.path.split("/").length - b.path.split("/").length;
257
+ });
258
+
259
+ for (const entry of sorted) {
260
+ if (entry.path === "/") continue;
261
+ if (entry.isDirectory) {
262
+ if (!vol.existsSync(entry.path)) vol.mkdirSync(entry.path, { recursive: true });
263
+ } else {
264
+ const parentDir = entry.path.substring(0, entry.path.lastIndexOf("/")) || "/";
265
+ if (parentDir !== "/" && !vol.existsSync(parentDir)) {
266
+ vol.mkdirSync(parentDir, { recursive: true });
267
+ }
268
+ const content = fullData.slice(entry.offset, entry.offset + entry.length);
269
+ vol.writeInternal(entry.path, content, false);
270
+ }
271
+ }
272
+
273
+ return vol;
274
+ }
275
+
276
+ static fromSnapshot(snapshot: VolumeSnapshot): MemoryVolume {
277
+ const vol = new MemoryVolume();
278
+
279
+ const sorted = snapshot.entries
280
+ .map((entry, idx) => ({ entry, depth: entry.path.split('/').length, idx }))
281
+ .sort((a, b) => a.depth - b.depth || a.idx - b.idx)
282
+ .map(x => x.entry);
283
+
284
+ for (const entry of sorted) {
285
+ if (entry.path === '/') continue;
286
+
287
+ if (entry.kind === 'directory') {
288
+ vol.mkdirSync(entry.path, { recursive: true });
289
+ } else if (entry.kind === 'file') {
290
+ let content: Uint8Array;
291
+ if (entry.data) {
292
+ content = base64ToBytes(entry.data);
293
+ } else {
294
+ content = new Uint8Array(0);
295
+ }
296
+ const parentDir = entry.path.substring(0, entry.path.lastIndexOf('/')) || '/';
297
+ if (parentDir !== '/' && !vol.existsSync(parentDir)) {
298
+ vol.mkdirSync(parentDir, { recursive: true });
299
+ }
300
+ vol.writeInternal(entry.path, content, false);
301
+ }
302
+ }
303
+
304
+ return vol;
305
+ }
306
+
307
+ // ---- Path utilities ----
308
+
309
+ private normalize(p: string): string {
310
+ if (!p.startsWith('/')) p = '/' + p;
311
+ const parts = p.split('/').filter(Boolean);
312
+ const resolved: string[] = [];
313
+ for (const part of parts) {
314
+ if (part === '..') resolved.pop();
315
+ else if (part !== '.') resolved.push(part);
316
+ }
317
+ return '/' + resolved.join('/');
318
+ }
319
+
320
+ private segments(p: string): string[] {
321
+ return this.normalize(p).split('/').filter(Boolean);
322
+ }
323
+
324
+ private parentOf(p: string): string {
325
+ const norm = this.normalize(p);
326
+ const idx = norm.lastIndexOf('/');
327
+ return idx <= 0 ? '/' : norm.slice(0, idx);
328
+ }
329
+
330
+ private nameOf(p: string): string {
331
+ const norm = this.normalize(p);
332
+ const idx = norm.lastIndexOf('/');
333
+ return norm.slice(idx + 1);
334
+ }
335
+
336
+ private locateRaw(p: string): VolumeNode | undefined {
337
+ const segs = this.segments(p);
338
+ let current = this.tree;
339
+ for (const seg of segs) {
340
+ if (current.kind === 'symlink') {
341
+ const resolved = this.locateRaw(current.target!);
342
+ if (!resolved || resolved.kind !== 'directory') return undefined;
343
+ current = resolved;
344
+ }
345
+ if (current.kind !== 'directory' || !current.children) return undefined;
346
+ const child = current.children.get(seg);
347
+ if (!child) return undefined;
348
+ current = child;
349
+ }
350
+ return current;
351
+ }
352
+
353
+ private locate(p: string): VolumeNode | undefined {
354
+ const node = this.locateRaw(p);
355
+ if (!node) return undefined;
356
+ // follow final symlink
357
+ if (node.kind === 'symlink') {
358
+ return this.locate(node.target!);
359
+ }
360
+ return node;
361
+ }
362
+
363
+ private ensureDir(p: string): VolumeNode {
364
+ const segs = this.segments(p);
365
+ let current = this.tree;
366
+ for (const seg of segs) {
367
+ if (!current.children) current.children = new Map();
368
+ let child = current.children.get(seg);
369
+ if (!child) {
370
+ child = { kind: 'directory', children: new Map(), modified: Date.now() };
371
+ current.children.set(seg, child);
372
+ } else if (child.kind !== 'directory') {
373
+ throw new Error(`ENOTDIR: not a directory, '${p}'`);
374
+ }
375
+ current = child;
376
+ }
377
+ return current;
378
+ }
379
+
380
+ // ---- Internal write ----
381
+
382
+ private writeInternal(p: string, data: string | Uint8Array, notify: boolean): void {
383
+ const norm = this.normalize(p);
384
+ const parentPath = this.parentOf(norm);
385
+ const name = this.nameOf(norm);
386
+
387
+ if (!name) {
388
+ throw new Error(`EISDIR: illegal operation on a directory, '${p}'`);
389
+ }
390
+
391
+ const parent = this.ensureDir(parentPath);
392
+ const existed = parent.children!.has(name);
393
+ const bytes = typeof data === 'string' ? this.textEncoder.encode(data) : data;
394
+
395
+ parent.children!.set(name, {
396
+ kind: 'file',
397
+ content: bytes,
398
+ modified: Date.now(),
399
+ });
400
+
401
+ if (notify) {
402
+ this.triggerWatchers(norm, existed ? 'change' : 'rename');
403
+ this.broadcast('change', norm, typeof data === 'string' ? data : this.textDecoder.decode(data));
404
+ this.notifyGlobalListeners(norm, existed ? 'change' : 'add');
405
+ }
406
+ }
407
+
408
+ // ---- Public synchronous API ----
409
+
410
+ existsSync(p: string): boolean {
411
+ return this.locate(p) !== undefined;
412
+ }
413
+
414
+ statSync(p: string): FileStat {
415
+ const node = this.locate(p);
416
+ if (!node) throw makeSystemError('ENOENT', 'stat', p);
417
+
418
+ const fileSize = node.kind === 'file' ? (node.content?.length || 0) : 0;
419
+ const ts = node.modified;
420
+
421
+ return {
422
+ isFile: () => node.kind === 'file',
423
+ isDirectory: () => node.kind === 'directory',
424
+ isSymbolicLink: () => false,
425
+ isBlockDevice: () => false,
426
+ isCharacterDevice: () => false,
427
+ isFIFO: () => false,
428
+ isSocket: () => false,
429
+ size: fileSize,
430
+ mode: node.kind === 'directory' ? 0o755 : 0o644,
431
+ mtime: new Date(ts),
432
+ atime: new Date(ts),
433
+ ctime: new Date(ts),
434
+ birthtime: new Date(ts),
435
+ mtimeMs: ts,
436
+ atimeMs: ts,
437
+ ctimeMs: ts,
438
+ birthtimeMs: ts,
439
+ nlink: 1,
440
+ uid: MOCK_IDS.UID,
441
+ gid: MOCK_IDS.GID,
442
+ dev: 0,
443
+ ino: 0,
444
+ rdev: 0,
445
+ blksize: MOCK_FS.BLOCK_SIZE,
446
+ blocks: Math.ceil(fileSize / MOCK_FS.BLOCK_CALC_SIZE),
447
+ atimeNs: BigInt(ts) * 1000000n,
448
+ mtimeNs: BigInt(ts) * 1000000n,
449
+ ctimeNs: BigInt(ts) * 1000000n,
450
+ birthtimeNs: BigInt(ts) * 1000000n,
451
+ };
452
+ }
453
+
454
+ lstatSync(p: string): FileStat {
455
+ const node = this.locateRaw(p);
456
+ if (!node) throw makeSystemError('ENOENT', 'lstat', p);
457
+
458
+ if (node.kind === 'symlink') {
459
+ const ts = node.modified;
460
+ return {
461
+ isFile: () => false,
462
+ isDirectory: () => false,
463
+ isSymbolicLink: () => true,
464
+ isBlockDevice: () => false,
465
+ isCharacterDevice: () => false,
466
+ isFIFO: () => false,
467
+ isSocket: () => false,
468
+ size: (node.target || '').length,
469
+ mode: 0o120777,
470
+ mtime: new Date(ts),
471
+ atime: new Date(ts),
472
+ ctime: new Date(ts),
473
+ birthtime: new Date(ts),
474
+ mtimeMs: ts,
475
+ atimeMs: ts,
476
+ ctimeMs: ts,
477
+ birthtimeMs: ts,
478
+ nlink: 1,
479
+ uid: 1000,
480
+ gid: 1000,
481
+ dev: 0,
482
+ ino: 0,
483
+ rdev: 0,
484
+ blksize: MOCK_FS.BLOCK_SIZE,
485
+ blocks: 0,
486
+ atimeNs: BigInt(ts) * 1000000n,
487
+ mtimeNs: BigInt(ts) * 1000000n,
488
+ ctimeNs: BigInt(ts) * 1000000n,
489
+ birthtimeNs: BigInt(ts) * 1000000n,
490
+ };
491
+ }
492
+ return this.statSync(p);
493
+ }
494
+
495
+ readFileSync(p: string): Uint8Array;
496
+ readFileSync(p: string, encoding: 'utf8' | 'utf-8'): string;
497
+ readFileSync(p: string, encoding?: 'utf8' | 'utf-8'): Uint8Array | string {
498
+ const node = this.locate(p);
499
+ if (!node) throw makeSystemError('ENOENT', 'open', p);
500
+ if (node.kind !== 'file') throw makeSystemError('EISDIR', 'read', p);
501
+
502
+ const bytes = node.content || new Uint8Array(0);
503
+ if (encoding === 'utf8' || encoding === 'utf-8') {
504
+ return this.textDecoder.decode(bytes);
505
+ }
506
+ return bytes;
507
+ }
508
+
509
+ writeFileSync(p: string, data: string | Uint8Array): void {
510
+ const norm = this.normalize(p);
511
+ const existed = this.locate(norm) !== undefined;
512
+ this.writeInternal(p, data, true);
513
+ }
514
+
515
+ mkdirSync(p: string, options?: { recursive?: boolean }): void {
516
+ const norm = this.normalize(p);
517
+
518
+ if (options?.recursive) {
519
+ this.ensureDir(norm);
520
+ return;
521
+ }
522
+
523
+ const parentPath = this.parentOf(norm);
524
+ const name = this.nameOf(norm);
525
+ if (!name) return;
526
+
527
+ const parent = this.locate(parentPath);
528
+ if (!parent) throw makeSystemError('ENOENT', 'mkdir', parentPath);
529
+ if (parent.kind !== 'directory') throw makeSystemError('ENOTDIR', 'mkdir', parentPath);
530
+ if (parent.children!.has(name)) throw makeSystemError('EEXIST', 'mkdir', p);
531
+
532
+ parent.children!.set(name, {
533
+ kind: 'directory',
534
+ children: new Map(),
535
+ modified: Date.now(),
536
+ });
537
+ }
538
+
539
+ readdirSync(p: string): string[] {
540
+ const node = this.locate(p);
541
+ if (!node) throw makeSystemError('ENOENT', 'scandir', p);
542
+ if (node.kind !== 'directory') throw makeSystemError('ENOTDIR', 'scandir', p);
543
+ return Array.from(node.children!.keys());
544
+ }
545
+
546
+ unlinkSync(p: string): void {
547
+ const norm = this.normalize(p);
548
+ const parentPath = this.parentOf(norm);
549
+ const name = this.nameOf(norm);
550
+
551
+ const parent = this.locate(parentPath);
552
+ if (!parent || parent.kind !== 'directory') throw makeSystemError('ENOENT', 'unlink', p);
553
+
554
+ const target = parent.children!.get(name);
555
+ if (!target) throw makeSystemError('ENOENT', 'unlink', p);
556
+ if (target.kind !== 'file') throw makeSystemError('EISDIR', 'unlink', p);
557
+
558
+ parent.children!.delete(name);
559
+ this.triggerWatchers(norm, 'rename');
560
+ this.broadcast('delete', norm);
561
+ this.notifyGlobalListeners(norm, 'unlink');
562
+ }
563
+
564
+ rmdirSync(p: string): void {
565
+ const norm = this.normalize(p);
566
+ const parentPath = this.parentOf(norm);
567
+ const name = this.nameOf(norm);
568
+
569
+ if (!name) throw new Error(`EPERM: operation not permitted, '${p}'`);
570
+
571
+ const parent = this.locate(parentPath);
572
+ if (!parent || parent.kind !== 'directory') throw makeSystemError('ENOENT', 'rmdir', p);
573
+
574
+ const target = parent.children!.get(name);
575
+ if (!target) throw makeSystemError('ENOENT', 'rmdir', p);
576
+ if (target.kind !== 'directory') throw makeSystemError('ENOTDIR', 'rmdir', p);
577
+ if (target.children!.size > 0) throw makeSystemError('ENOTEMPTY', 'rmdir', p);
578
+
579
+ parent.children!.delete(name);
580
+ }
581
+
582
+ renameSync(from: string, to: string): void {
583
+ const normFrom = this.normalize(from);
584
+ const normTo = this.normalize(to);
585
+
586
+ const fromParent = this.locate(this.parentOf(normFrom));
587
+ if (!fromParent || fromParent.kind !== 'directory') throw makeSystemError('ENOENT', 'rename', from);
588
+
589
+ const fromName = this.nameOf(normFrom);
590
+ const node = fromParent.children!.get(fromName);
591
+ if (!node) throw makeSystemError('ENOENT', 'rename', from);
592
+
593
+ const toParent = this.ensureDir(this.parentOf(normTo));
594
+ const toName = this.nameOf(normTo);
595
+
596
+ fromParent.children!.delete(fromName);
597
+ toParent.children!.set(toName, node);
598
+
599
+ this.triggerWatchers(normFrom, 'rename');
600
+ this.triggerWatchers(normTo, 'rename');
601
+ this.notifyGlobalListeners(normFrom, 'unlink');
602
+ this.notifyGlobalListeners(normTo, 'add');
603
+ }
604
+
605
+ accessSync(p: string, _mode?: number): void {
606
+ if (!this.existsSync(p)) throw makeSystemError('ENOENT', 'access', p);
607
+ }
608
+
609
+ copyFileSync(src: string, dest: string): void {
610
+ const data = this.readFileSync(src);
611
+ this.writeFileSync(dest, data);
612
+ }
613
+
614
+ realpathSync(p: string): string {
615
+ const norm = this.normalize(p);
616
+ const node = this.locateRaw(norm);
617
+ if (!node) throw makeSystemError('ENOENT', 'realpath', p);
618
+ if (node.kind === 'symlink') {
619
+ return this.realpathSync(node.target!);
620
+ }
621
+ return norm;
622
+ }
623
+
624
+ symlinkSync(target: string, linkPath: string, _type?: string): void {
625
+ const normLink = this.normalize(linkPath);
626
+ const parentPath = this.parentOf(normLink);
627
+ const name = this.nameOf(normLink);
628
+
629
+ if (!name) throw new Error(`EISDIR: invalid symlink path, '${linkPath}'`);
630
+ const parent = this.ensureDir(parentPath);
631
+
632
+ parent.children!.set(name, {
633
+ kind: 'symlink',
634
+ target: this.normalize(target),
635
+ modified: Date.now(),
636
+ });
637
+ }
638
+
639
+ readlinkSync(p: string): string {
640
+ const norm = this.normalize(p);
641
+ const node = this.locateRaw(norm);
642
+ if (!node) throw makeSystemError('ENOENT', 'readlink', p);
643
+ if (node.kind !== 'symlink') {
644
+ const err = new Error(`EINVAL: invalid argument, readlink '${p}'`) as SystemError;
645
+ err.code = 'EINVAL';
646
+ err.errno = -22;
647
+ err.syscall = 'readlink';
648
+ err.path = p;
649
+ throw err;
650
+ }
651
+ return node.target!;
652
+ }
653
+
654
+ linkSync(existingPath: string, newPath: string): void {
655
+ // hard links share the same content reference
656
+ const existing = this.locate(existingPath);
657
+ if (!existing) throw makeSystemError('ENOENT', 'link', existingPath);
658
+ if (existing.kind !== 'file') throw makeSystemError('EISDIR', 'link', existingPath);
659
+
660
+ const normNew = this.normalize(newPath);
661
+ const parentPath = this.parentOf(normNew);
662
+ const name = this.nameOf(normNew);
663
+ const parent = this.ensureDir(parentPath);
664
+
665
+ parent.children!.set(name, {
666
+ kind: 'file',
667
+ content: existing.content,
668
+ modified: existing.modified,
669
+ });
670
+ }
671
+
672
+ chmodSync(_p: string, _mode: number): void {
673
+ // no-op besides existence check, VFS doesn't track permissions
674
+ const norm = this.normalize(_p);
675
+ if (!this.locate(norm)) throw makeSystemError('ENOENT', 'chmod', _p);
676
+ }
677
+
678
+ chownSync(_p: string, _uid: number, _gid: number): void {
679
+ const norm = this.normalize(_p);
680
+ if (!this.locate(norm)) throw makeSystemError('ENOENT', 'chown', _p);
681
+ }
682
+
683
+ appendFileSync(p: string, data: string | Uint8Array): void {
684
+ const norm = this.normalize(p);
685
+ let existing: Uint8Array = new Uint8Array(0);
686
+ try {
687
+ existing = new Uint8Array(this.readFileSync(norm));
688
+ } catch { /* file doesn't exist yet */ }
689
+ const bytes = typeof data === 'string' ? this.textEncoder.encode(data) : data;
690
+ const combined = new Uint8Array(existing.length + bytes.length);
691
+ combined.set(existing);
692
+ combined.set(bytes, existing.length);
693
+ this.writeFileSync(norm, combined);
694
+ }
695
+
696
+ truncateSync(p: string, len: number = 0): void {
697
+ const norm = this.normalize(p);
698
+ const node = this.locate(norm);
699
+ if (!node) throw makeSystemError('ENOENT', 'truncate', p);
700
+ if (node.kind !== 'file') throw makeSystemError('EISDIR', 'truncate', p);
701
+ const content = node.content || new Uint8Array(0);
702
+ if (len < content.length) {
703
+ node.content = content.slice(0, len);
704
+ } else if (len > content.length) {
705
+ const bigger = new Uint8Array(len);
706
+ bigger.set(content);
707
+ node.content = bigger;
708
+ }
709
+ node.modified = Date.now();
710
+ }
711
+
712
+ // ---- Async wrappers ----
713
+
714
+ readFile(
715
+ p: string,
716
+ optionsOrCb?: { encoding?: string } | ((err: Error | null, data?: Uint8Array | string) => void),
717
+ cb?: (err: Error | null, data?: Uint8Array | string) => void
718
+ ): void {
719
+ const actualCb = typeof optionsOrCb === 'function' ? optionsOrCb : cb;
720
+ const opts = typeof optionsOrCb === 'object' ? optionsOrCb : undefined;
721
+ try {
722
+ const data = opts?.encoding
723
+ ? this.readFileSync(p, opts.encoding as 'utf8')
724
+ : this.readFileSync(p);
725
+ if (actualCb) setTimeout(() => actualCb(null, data), 0);
726
+ } catch (err) {
727
+ if (actualCb) setTimeout(() => actualCb(err as Error), 0);
728
+ }
729
+ }
730
+
731
+ stat(p: string, cb?: (err: Error | null, stats?: FileStat) => void): void {
732
+ try {
733
+ const stats = this.statSync(p);
734
+ if (cb) setTimeout(() => cb(null, stats), 0);
735
+ } catch (err) {
736
+ if (cb) setTimeout(() => cb(err as Error), 0);
737
+ }
738
+ }
739
+
740
+ lstat(p: string, cb?: (err: Error | null, stats?: FileStat) => void): void {
741
+ this.stat(p, cb);
742
+ }
743
+
744
+ readdir(
745
+ p: string,
746
+ optionsOrCb?: { withFileTypes?: boolean } | ((err: Error | null, files?: string[]) => void),
747
+ cb?: (err: Error | null, files?: string[]) => void
748
+ ): void {
749
+ const actualCb = typeof optionsOrCb === 'function' ? optionsOrCb : cb;
750
+ try {
751
+ const files = this.readdirSync(p);
752
+ if (actualCb) setTimeout(() => actualCb(null, files), 0);
753
+ } catch (err) {
754
+ if (actualCb) setTimeout(() => actualCb(err as Error), 0);
755
+ }
756
+ }
757
+
758
+ realpath(p: string, cb?: (err: Error | null, resolved?: string) => void): void {
759
+ try {
760
+ const resolved = this.realpathSync(p);
761
+ if (cb) setTimeout(() => cb(null, resolved), 0);
762
+ } catch (err) {
763
+ if (cb) setTimeout(() => cb(err as Error), 0);
764
+ }
765
+ }
766
+
767
+ access(p: string, modeOrCb?: number | ((err: Error | null) => void), cb?: (err: Error | null) => void): void {
768
+ const actualCb = typeof modeOrCb === 'function' ? modeOrCb : cb;
769
+ try {
770
+ this.accessSync(p);
771
+ if (actualCb) setTimeout(() => actualCb(null), 0);
772
+ } catch (err) {
773
+ if (actualCb) setTimeout(() => actualCb(err as Error), 0);
774
+ }
775
+ }
776
+
777
+ // ---- File watchers ----
778
+
779
+ watch(
780
+ target: string,
781
+ optionsOrCb?: { persistent?: boolean; recursive?: boolean; encoding?: string } | WatchCallback,
782
+ cb?: WatchCallback
783
+ ): FileWatchHandle {
784
+ const norm = this.normalize(target);
785
+
786
+ let opts: { persistent?: boolean; recursive?: boolean } = {};
787
+ let actualCb: WatchCallback | undefined;
788
+
789
+ if (typeof optionsOrCb === 'function') {
790
+ actualCb = optionsOrCb;
791
+ } else if (optionsOrCb) {
792
+ opts = optionsOrCb;
793
+ actualCb = cb;
794
+ } else {
795
+ actualCb = cb;
796
+ }
797
+
798
+ const handle = new FSWatcher(() => {
799
+ watcher.active = false;
800
+ const set = this.activeWatchers.get(norm);
801
+ if (set) {
802
+ set.delete(watcher);
803
+ if (set.size === 0) this.activeWatchers.delete(norm);
804
+ }
805
+ });
806
+
807
+ const watcher: ActiveWatcher = {
808
+ callback: (event, filename) => {
809
+ if (actualCb) actualCb(event, filename);
810
+ handle.emit('change', event, filename);
811
+ },
812
+ recursive: opts.recursive || false,
813
+ active: true,
814
+ };
815
+
816
+ if (!this.activeWatchers.has(norm)) {
817
+ this.activeWatchers.set(norm, new Set());
818
+ }
819
+ this.activeWatchers.get(norm)!.add(watcher);
820
+
821
+ return handle;
822
+ }
823
+
824
+ private triggerWatchers(changedPath: string, event: WatchEventKind): void {
825
+ const norm = this.normalize(changedPath);
826
+ const fileName = this.nameOf(norm);
827
+
828
+ const direct = this.activeWatchers.get(norm);
829
+ if (direct) {
830
+ for (const w of direct) {
831
+ if (w.active) {
832
+ try { w.callback(event, fileName); } catch (e) { console.error('Watcher error:', e); }
833
+ }
834
+ }
835
+ }
836
+
837
+ // walk up the tree to notify parent/recursive watchers
838
+ let current = this.parentOf(norm);
839
+ let relative = fileName;
840
+
841
+ while (current) {
842
+ const parentWatchers = this.activeWatchers.get(current);
843
+ if (parentWatchers) {
844
+ for (const w of parentWatchers) {
845
+ if (w.active) {
846
+ const isDirectChild = this.parentOf(norm) === current;
847
+ if (w.recursive || isDirectChild) {
848
+ try { w.callback(event, relative); } catch (e) { console.error('Watcher error:', e); }
849
+ }
850
+ }
851
+ }
852
+ }
853
+
854
+ if (current === '/') break;
855
+ relative = this.nameOf(current) + '/' + relative;
856
+ current = this.parentOf(current);
857
+ }
858
+
859
+ }
860
+
861
+ // ---- Global change listeners (for chokidar/HMR bridging) ----
862
+ private globalChangeListeners = new Set<(path: string, event: string) => void>();
863
+
864
+ onGlobalChange(cb: (path: string, event: string) => void): () => void {
865
+ this.globalChangeListeners.add(cb);
866
+ return () => { this.globalChangeListeners.delete(cb); };
867
+ }
868
+
869
+ private notifyGlobalListeners(path: string, event: string): void {
870
+ for (const cb of this.globalChangeListeners) {
871
+ try { cb(path, event); } catch (e) { console.error('Global VFS listener error:', e); }
872
+ }
873
+ }
874
+
875
+ // ---- Stream-like APIs ----
876
+
877
+ createReadStream(p: string): {
878
+ on: (event: string, cb: (...args: unknown[]) => void) => void;
879
+ pipe: (dest: unknown) => unknown;
880
+ } {
881
+ const self = this;
882
+ const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
883
+
884
+ const readable = {
885
+ on(event: string, cb: (...args: unknown[]) => void) {
886
+ if (!handlers[event]) handlers[event] = [];
887
+ handlers[event].push(cb);
888
+ return readable;
889
+ },
890
+ pipe(dest: unknown) { return dest; },
891
+ };
892
+
893
+ setTimeout(() => {
894
+ try {
895
+ const data = self.readFileSync(p);
896
+ handlers['data']?.forEach(cb => cb(data));
897
+ handlers['end']?.forEach(cb => cb());
898
+ } catch (err) {
899
+ handlers['error']?.forEach(cb => cb(err));
900
+ }
901
+ }, 0);
902
+
903
+ return readable;
904
+ }
905
+
906
+ createWriteStream(p: string): {
907
+ write: (data: string | Uint8Array) => boolean;
908
+ end: (data?: string | Uint8Array) => void;
909
+ on: (event: string, cb: (...args: unknown[]) => void) => void;
910
+ } {
911
+ const self = this;
912
+ const pending: Uint8Array[] = [];
913
+ const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
914
+ const enc = new TextEncoder();
915
+
916
+ return {
917
+ write(data: string | Uint8Array): boolean {
918
+ pending.push(typeof data === 'string' ? enc.encode(data) : data);
919
+ return true;
920
+ },
921
+ end(data?: string | Uint8Array): void {
922
+ if (data) pending.push(typeof data === 'string' ? enc.encode(data) : data);
923
+ const totalLen = pending.reduce((sum, chunk) => sum + chunk.length, 0);
924
+ const merged = new Uint8Array(totalLen);
925
+ let pos = 0;
926
+ for (const chunk of pending) {
927
+ merged.set(chunk, pos);
928
+ pos += chunk.length;
929
+ }
930
+ self.writeFileSync(p, merged);
931
+ handlers['finish']?.forEach(cb => cb());
932
+ handlers['close']?.forEach(cb => cb());
933
+ },
934
+ on(event: string, cb: (...args: unknown[]) => void) {
935
+ if (!handlers[event]) handlers[event] = [];
936
+ handlers[event].push(cb);
937
+ return this;
938
+ },
939
+ };
940
+ }
941
+ }