almostnode 0.1.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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +731 -0
  3. package/dist/__sw__.js +394 -0
  4. package/dist/ai-chatbot-demo-entry.d.ts +6 -0
  5. package/dist/ai-chatbot-demo-entry.d.ts.map +1 -0
  6. package/dist/ai-chatbot-demo.d.ts +42 -0
  7. package/dist/ai-chatbot-demo.d.ts.map +1 -0
  8. package/dist/assets/runtime-worker-D9x_Ddwz.js +60543 -0
  9. package/dist/assets/runtime-worker-D9x_Ddwz.js.map +1 -0
  10. package/dist/convex-app-demo-entry.d.ts +6 -0
  11. package/dist/convex-app-demo-entry.d.ts.map +1 -0
  12. package/dist/convex-app-demo.d.ts +68 -0
  13. package/dist/convex-app-demo.d.ts.map +1 -0
  14. package/dist/cors-proxy.d.ts +46 -0
  15. package/dist/cors-proxy.d.ts.map +1 -0
  16. package/dist/create-runtime.d.ts +42 -0
  17. package/dist/create-runtime.d.ts.map +1 -0
  18. package/dist/demo.d.ts +6 -0
  19. package/dist/demo.d.ts.map +1 -0
  20. package/dist/dev-server.d.ts +97 -0
  21. package/dist/dev-server.d.ts.map +1 -0
  22. package/dist/frameworks/next-dev-server.d.ts +202 -0
  23. package/dist/frameworks/next-dev-server.d.ts.map +1 -0
  24. package/dist/frameworks/vite-dev-server.d.ts +85 -0
  25. package/dist/frameworks/vite-dev-server.d.ts.map +1 -0
  26. package/dist/index.cjs +14965 -0
  27. package/dist/index.cjs.map +1 -0
  28. package/dist/index.d.ts +71 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.mjs +14867 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/dist/next-demo.d.ts +49 -0
  33. package/dist/next-demo.d.ts.map +1 -0
  34. package/dist/npm/index.d.ts +71 -0
  35. package/dist/npm/index.d.ts.map +1 -0
  36. package/dist/npm/registry.d.ts +66 -0
  37. package/dist/npm/registry.d.ts.map +1 -0
  38. package/dist/npm/resolver.d.ts +52 -0
  39. package/dist/npm/resolver.d.ts.map +1 -0
  40. package/dist/npm/tarball.d.ts +29 -0
  41. package/dist/npm/tarball.d.ts.map +1 -0
  42. package/dist/runtime-interface.d.ts +90 -0
  43. package/dist/runtime-interface.d.ts.map +1 -0
  44. package/dist/runtime.d.ts +103 -0
  45. package/dist/runtime.d.ts.map +1 -0
  46. package/dist/sandbox-helpers.d.ts +43 -0
  47. package/dist/sandbox-helpers.d.ts.map +1 -0
  48. package/dist/sandbox-runtime.d.ts +65 -0
  49. package/dist/sandbox-runtime.d.ts.map +1 -0
  50. package/dist/server-bridge.d.ts +89 -0
  51. package/dist/server-bridge.d.ts.map +1 -0
  52. package/dist/shims/assert.d.ts +51 -0
  53. package/dist/shims/assert.d.ts.map +1 -0
  54. package/dist/shims/async_hooks.d.ts +37 -0
  55. package/dist/shims/async_hooks.d.ts.map +1 -0
  56. package/dist/shims/buffer.d.ts +20 -0
  57. package/dist/shims/buffer.d.ts.map +1 -0
  58. package/dist/shims/child_process-browser.d.ts +92 -0
  59. package/dist/shims/child_process-browser.d.ts.map +1 -0
  60. package/dist/shims/child_process.d.ts +93 -0
  61. package/dist/shims/child_process.d.ts.map +1 -0
  62. package/dist/shims/chokidar.d.ts +55 -0
  63. package/dist/shims/chokidar.d.ts.map +1 -0
  64. package/dist/shims/cluster.d.ts +52 -0
  65. package/dist/shims/cluster.d.ts.map +1 -0
  66. package/dist/shims/crypto.d.ts +122 -0
  67. package/dist/shims/crypto.d.ts.map +1 -0
  68. package/dist/shims/dgram.d.ts +34 -0
  69. package/dist/shims/dgram.d.ts.map +1 -0
  70. package/dist/shims/diagnostics_channel.d.ts +80 -0
  71. package/dist/shims/diagnostics_channel.d.ts.map +1 -0
  72. package/dist/shims/dns.d.ts +87 -0
  73. package/dist/shims/dns.d.ts.map +1 -0
  74. package/dist/shims/domain.d.ts +25 -0
  75. package/dist/shims/domain.d.ts.map +1 -0
  76. package/dist/shims/esbuild.d.ts +105 -0
  77. package/dist/shims/esbuild.d.ts.map +1 -0
  78. package/dist/shims/events.d.ts +37 -0
  79. package/dist/shims/events.d.ts.map +1 -0
  80. package/dist/shims/fs.d.ts +115 -0
  81. package/dist/shims/fs.d.ts.map +1 -0
  82. package/dist/shims/fsevents.d.ts +67 -0
  83. package/dist/shims/fsevents.d.ts.map +1 -0
  84. package/dist/shims/http.d.ts +217 -0
  85. package/dist/shims/http.d.ts.map +1 -0
  86. package/dist/shims/http2.d.ts +81 -0
  87. package/dist/shims/http2.d.ts.map +1 -0
  88. package/dist/shims/https.d.ts +36 -0
  89. package/dist/shims/https.d.ts.map +1 -0
  90. package/dist/shims/inspector.d.ts +25 -0
  91. package/dist/shims/inspector.d.ts.map +1 -0
  92. package/dist/shims/module.d.ts +22 -0
  93. package/dist/shims/module.d.ts.map +1 -0
  94. package/dist/shims/net.d.ts +100 -0
  95. package/dist/shims/net.d.ts.map +1 -0
  96. package/dist/shims/os.d.ts +159 -0
  97. package/dist/shims/os.d.ts.map +1 -0
  98. package/dist/shims/path.d.ts +72 -0
  99. package/dist/shims/path.d.ts.map +1 -0
  100. package/dist/shims/perf_hooks.d.ts +50 -0
  101. package/dist/shims/perf_hooks.d.ts.map +1 -0
  102. package/dist/shims/process.d.ts +93 -0
  103. package/dist/shims/process.d.ts.map +1 -0
  104. package/dist/shims/querystring.d.ts +23 -0
  105. package/dist/shims/querystring.d.ts.map +1 -0
  106. package/dist/shims/readdirp.d.ts +52 -0
  107. package/dist/shims/readdirp.d.ts.map +1 -0
  108. package/dist/shims/readline.d.ts +62 -0
  109. package/dist/shims/readline.d.ts.map +1 -0
  110. package/dist/shims/rollup.d.ts +34 -0
  111. package/dist/shims/rollup.d.ts.map +1 -0
  112. package/dist/shims/sentry.d.ts +163 -0
  113. package/dist/shims/sentry.d.ts.map +1 -0
  114. package/dist/shims/stream.d.ts +181 -0
  115. package/dist/shims/stream.d.ts.map +1 -0
  116. package/dist/shims/tls.d.ts +53 -0
  117. package/dist/shims/tls.d.ts.map +1 -0
  118. package/dist/shims/tty.d.ts +30 -0
  119. package/dist/shims/tty.d.ts.map +1 -0
  120. package/dist/shims/url.d.ts +64 -0
  121. package/dist/shims/url.d.ts.map +1 -0
  122. package/dist/shims/util.d.ts +106 -0
  123. package/dist/shims/util.d.ts.map +1 -0
  124. package/dist/shims/v8.d.ts +73 -0
  125. package/dist/shims/v8.d.ts.map +1 -0
  126. package/dist/shims/vfs-adapter.d.ts +126 -0
  127. package/dist/shims/vfs-adapter.d.ts.map +1 -0
  128. package/dist/shims/vm.d.ts +45 -0
  129. package/dist/shims/vm.d.ts.map +1 -0
  130. package/dist/shims/worker_threads.d.ts +66 -0
  131. package/dist/shims/worker_threads.d.ts.map +1 -0
  132. package/dist/shims/ws.d.ts +66 -0
  133. package/dist/shims/ws.d.ts.map +1 -0
  134. package/dist/shims/zlib.d.ts +161 -0
  135. package/dist/shims/zlib.d.ts.map +1 -0
  136. package/dist/transform.d.ts +24 -0
  137. package/dist/transform.d.ts.map +1 -0
  138. package/dist/virtual-fs.d.ts +226 -0
  139. package/dist/virtual-fs.d.ts.map +1 -0
  140. package/dist/vite-demo.d.ts +35 -0
  141. package/dist/vite-demo.d.ts.map +1 -0
  142. package/dist/vite-sw.js +132 -0
  143. package/dist/worker/runtime-worker.d.ts +8 -0
  144. package/dist/worker/runtime-worker.d.ts.map +1 -0
  145. package/dist/worker-runtime.d.ts +50 -0
  146. package/dist/worker-runtime.d.ts.map +1 -0
  147. package/package.json +85 -0
  148. package/src/ai-chatbot-demo-entry.ts +244 -0
  149. package/src/ai-chatbot-demo.ts +509 -0
  150. package/src/convex-app-demo-entry.ts +1107 -0
  151. package/src/convex-app-demo.ts +1316 -0
  152. package/src/cors-proxy.ts +81 -0
  153. package/src/create-runtime.ts +147 -0
  154. package/src/demo.ts +304 -0
  155. package/src/dev-server.ts +274 -0
  156. package/src/frameworks/next-dev-server.ts +2224 -0
  157. package/src/frameworks/vite-dev-server.ts +702 -0
  158. package/src/index.ts +101 -0
  159. package/src/next-demo.ts +1784 -0
  160. package/src/npm/index.ts +347 -0
  161. package/src/npm/registry.ts +152 -0
  162. package/src/npm/resolver.ts +385 -0
  163. package/src/npm/tarball.ts +209 -0
  164. package/src/runtime-interface.ts +103 -0
  165. package/src/runtime.ts +1046 -0
  166. package/src/sandbox-helpers.ts +173 -0
  167. package/src/sandbox-runtime.ts +252 -0
  168. package/src/server-bridge.ts +426 -0
  169. package/src/shims/assert.ts +664 -0
  170. package/src/shims/async_hooks.ts +86 -0
  171. package/src/shims/buffer.ts +75 -0
  172. package/src/shims/child_process-browser.ts +217 -0
  173. package/src/shims/child_process.ts +463 -0
  174. package/src/shims/chokidar.ts +313 -0
  175. package/src/shims/cluster.ts +67 -0
  176. package/src/shims/crypto.ts +830 -0
  177. package/src/shims/dgram.ts +47 -0
  178. package/src/shims/diagnostics_channel.ts +196 -0
  179. package/src/shims/dns.ts +172 -0
  180. package/src/shims/domain.ts +58 -0
  181. package/src/shims/esbuild.ts +805 -0
  182. package/src/shims/events.ts +195 -0
  183. package/src/shims/fs.ts +803 -0
  184. package/src/shims/fsevents.ts +63 -0
  185. package/src/shims/http.ts +904 -0
  186. package/src/shims/http2.ts +96 -0
  187. package/src/shims/https.ts +86 -0
  188. package/src/shims/inspector.ts +30 -0
  189. package/src/shims/module.ts +82 -0
  190. package/src/shims/net.ts +359 -0
  191. package/src/shims/os.ts +195 -0
  192. package/src/shims/path.ts +199 -0
  193. package/src/shims/perf_hooks.ts +92 -0
  194. package/src/shims/process.ts +346 -0
  195. package/src/shims/querystring.ts +97 -0
  196. package/src/shims/readdirp.ts +228 -0
  197. package/src/shims/readline.ts +110 -0
  198. package/src/shims/rollup.ts +80 -0
  199. package/src/shims/sentry.ts +133 -0
  200. package/src/shims/stream.ts +1126 -0
  201. package/src/shims/tls.ts +95 -0
  202. package/src/shims/tty.ts +64 -0
  203. package/src/shims/url.ts +171 -0
  204. package/src/shims/util.ts +312 -0
  205. package/src/shims/v8.ts +113 -0
  206. package/src/shims/vfs-adapter.ts +402 -0
  207. package/src/shims/vm.ts +83 -0
  208. package/src/shims/worker_threads.ts +111 -0
  209. package/src/shims/ws.ts +382 -0
  210. package/src/shims/zlib.ts +289 -0
  211. package/src/transform.ts +313 -0
  212. package/src/types/external.d.ts +67 -0
  213. package/src/virtual-fs.ts +903 -0
  214. package/src/vite-demo.ts +577 -0
  215. package/src/worker/runtime-worker.ts +128 -0
  216. package/src/worker-runtime.ts +145 -0
@@ -0,0 +1,903 @@
1
+ /**
2
+ * Virtual File System - In-memory file tree with POSIX-like operations
3
+ */
4
+
5
+ import type { VFSSnapshot, VFSFileEntry } from './runtime-interface';
6
+
7
+ export interface FSNode {
8
+ type: 'file' | 'directory';
9
+ content?: Uint8Array;
10
+ children?: Map<string, FSNode>;
11
+ }
12
+
13
+ // Simple EventEmitter for VFS change notifications
14
+ type VFSChangeListener = (path: string, content: string) => void;
15
+ type VFSDeleteListener = (path: string) => void;
16
+ type VFSEventListener = VFSChangeListener | VFSDeleteListener;
17
+
18
+ export interface Stats {
19
+ isFile(): boolean;
20
+ isDirectory(): boolean;
21
+ isSymbolicLink(): boolean;
22
+ isBlockDevice(): boolean;
23
+ isCharacterDevice(): boolean;
24
+ isFIFO(): boolean;
25
+ isSocket(): boolean;
26
+ size: number;
27
+ mode: number;
28
+ mtime: Date;
29
+ atime: Date;
30
+ ctime: Date;
31
+ birthtime: Date;
32
+ mtimeMs: number;
33
+ atimeMs: number;
34
+ ctimeMs: number;
35
+ birthtimeMs: number;
36
+ nlink: number;
37
+ uid: number;
38
+ gid: number;
39
+ dev: number;
40
+ ino: number;
41
+ rdev: number;
42
+ blksize: number;
43
+ blocks: number;
44
+ }
45
+
46
+ export type WatchEventType = 'change' | 'rename';
47
+ export type WatchListener = (eventType: WatchEventType, filename: string | null) => void;
48
+
49
+ export interface FSWatcher {
50
+ close(): void;
51
+ ref(): this;
52
+ unref(): this;
53
+ }
54
+
55
+ interface WatcherEntry {
56
+ listener: WatchListener;
57
+ recursive: boolean;
58
+ closed: boolean;
59
+ }
60
+
61
+ /**
62
+ * Create a Node.js-style error with code property
63
+ */
64
+ export interface NodeError extends Error {
65
+ code: string;
66
+ errno: number;
67
+ syscall: string;
68
+ path?: string;
69
+ }
70
+
71
+ export function createNodeError(
72
+ code: 'ENOENT' | 'ENOTDIR' | 'EISDIR' | 'EEXIST' | 'ENOTEMPTY',
73
+ syscall: string,
74
+ path: string,
75
+ message?: string
76
+ ): NodeError {
77
+ const errno: Record<string, number> = {
78
+ ENOENT: -2,
79
+ ENOTDIR: -20,
80
+ EISDIR: -21,
81
+ EEXIST: -17,
82
+ ENOTEMPTY: -39,
83
+ };
84
+
85
+ const messages: Record<string, string> = {
86
+ ENOENT: 'no such file or directory',
87
+ ENOTDIR: 'not a directory',
88
+ EISDIR: 'is a directory',
89
+ EEXIST: 'file already exists',
90
+ ENOTEMPTY: 'directory not empty',
91
+ };
92
+
93
+ const err = new Error(
94
+ message || `${code}: ${messages[code]}, ${syscall} '${path}'`
95
+ ) as NodeError;
96
+ err.code = code;
97
+ err.errno = errno[code];
98
+ err.syscall = syscall;
99
+ err.path = path;
100
+ return err;
101
+ }
102
+
103
+ export class VirtualFS {
104
+ private root: FSNode;
105
+ private encoder = new TextEncoder();
106
+ private decoder = new TextDecoder();
107
+ private watchers = new Map<string, Set<WatcherEntry>>();
108
+ private eventListeners = new Map<string, Set<VFSEventListener>>();
109
+
110
+ constructor() {
111
+ this.root = {
112
+ type: 'directory',
113
+ children: new Map(),
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Add event listener (for change notifications to workers)
119
+ */
120
+ on(event: 'change', listener: VFSChangeListener): this;
121
+ on(event: 'delete', listener: VFSDeleteListener): this;
122
+ on(event: string, listener: VFSEventListener): this {
123
+ if (!this.eventListeners.has(event)) {
124
+ this.eventListeners.set(event, new Set());
125
+ }
126
+ this.eventListeners.get(event)!.add(listener);
127
+ return this;
128
+ }
129
+
130
+ /**
131
+ * Remove event listener
132
+ */
133
+ off(event: 'change', listener: VFSChangeListener): this;
134
+ off(event: 'delete', listener: VFSDeleteListener): this;
135
+ off(event: string, listener: VFSEventListener): this {
136
+ const listeners = this.eventListeners.get(event);
137
+ if (listeners) {
138
+ listeners.delete(listener);
139
+ }
140
+ return this;
141
+ }
142
+
143
+ /**
144
+ * Emit event to listeners
145
+ */
146
+ private emit(event: 'change', path: string, content: string): void;
147
+ private emit(event: 'delete', path: string): void;
148
+ private emit(event: string, ...args: unknown[]): void {
149
+ const listeners = this.eventListeners.get(event);
150
+ if (listeners) {
151
+ for (const listener of listeners) {
152
+ try {
153
+ (listener as (...args: unknown[]) => void)(...args);
154
+ } catch (err) {
155
+ console.error('Error in VFS event listener:', err);
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Serialize the entire file tree to a snapshot (for worker transfer)
163
+ */
164
+ toSnapshot(): VFSSnapshot {
165
+ const files: VFSFileEntry[] = [];
166
+ this.serializeNode('/', this.root, files);
167
+ return { files };
168
+ }
169
+
170
+ private serializeNode(path: string, node: FSNode, files: VFSFileEntry[]): void {
171
+ if (node.type === 'file') {
172
+ // Encode binary content as base64
173
+ let content = '';
174
+ if (node.content && node.content.length > 0) {
175
+ let binary = '';
176
+ for (let i = 0; i < node.content.length; i++) {
177
+ binary += String.fromCharCode(node.content[i]);
178
+ }
179
+ content = btoa(binary);
180
+ }
181
+ files.push({ path, type: 'file', content });
182
+ } else if (node.type === 'directory') {
183
+ files.push({ path, type: 'directory' });
184
+ if (node.children) {
185
+ for (const [name, child] of node.children) {
186
+ const childPath = path === '/' ? `/${name}` : `${path}/${name}`;
187
+ this.serializeNode(childPath, child, files);
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Create a VirtualFS from a snapshot
195
+ */
196
+ static fromSnapshot(snapshot: VFSSnapshot): VirtualFS {
197
+ const vfs = new VirtualFS();
198
+
199
+ // Sort entries to ensure directories are created before their contents
200
+ const sortedFiles = [...snapshot.files].sort((a, b) => {
201
+ const aDepth = a.path.split('/').length;
202
+ const bDepth = b.path.split('/').length;
203
+ return aDepth - bDepth;
204
+ });
205
+
206
+ for (const entry of sortedFiles) {
207
+ if (entry.path === '/') continue; // Skip root
208
+
209
+ if (entry.type === 'directory') {
210
+ vfs.mkdirSync(entry.path, { recursive: true });
211
+ } else if (entry.type === 'file') {
212
+ // Decode base64 content
213
+ let content: Uint8Array;
214
+ if (entry.content) {
215
+ const binary = atob(entry.content);
216
+ content = new Uint8Array(binary.length);
217
+ for (let i = 0; i < binary.length; i++) {
218
+ content[i] = binary.charCodeAt(i);
219
+ }
220
+ } else {
221
+ content = new Uint8Array(0);
222
+ }
223
+ // Ensure parent directory exists
224
+ const parentPath = entry.path.substring(0, entry.path.lastIndexOf('/')) || '/';
225
+ if (parentPath !== '/' && !vfs.existsSync(parentPath)) {
226
+ vfs.mkdirSync(parentPath, { recursive: true });
227
+ }
228
+ vfs.writeFileSyncInternal(entry.path, content, false); // Don't emit events during restore
229
+ }
230
+ }
231
+
232
+ return vfs;
233
+ }
234
+
235
+ /**
236
+ * Internal write that optionally emits events
237
+ */
238
+ private writeFileSyncInternal(path: string, data: string | Uint8Array, emitEvent: boolean): void {
239
+ const normalized = this.normalizePath(path);
240
+ const parentPath = this.getParentPath(normalized);
241
+ const basename = this.getBasename(normalized);
242
+
243
+ if (!basename) {
244
+ throw new Error(`EISDIR: illegal operation on a directory, '${path}'`);
245
+ }
246
+
247
+ const parent = this.ensureDirectory(parentPath);
248
+ const existed = parent.children!.has(basename);
249
+
250
+ const content = typeof data === 'string' ? this.encoder.encode(data) : data;
251
+
252
+ parent.children!.set(basename, {
253
+ type: 'file',
254
+ content,
255
+ });
256
+
257
+ if (emitEvent) {
258
+ // Notify watchers
259
+ this.notifyWatchers(normalized, existed ? 'change' : 'rename');
260
+ // Emit change event for worker sync
261
+ this.emit('change', normalized, typeof data === 'string' ? data : this.decoder.decode(data));
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Normalize path - resolve . and .. segments, ensure leading /
267
+ */
268
+ private normalizePath(path: string): string {
269
+ if (!path.startsWith('/')) {
270
+ path = '/' + path;
271
+ }
272
+
273
+ const parts = path.split('/').filter(Boolean);
274
+ const resolved: string[] = [];
275
+
276
+ for (const part of parts) {
277
+ if (part === '..') {
278
+ resolved.pop();
279
+ } else if (part !== '.') {
280
+ resolved.push(part);
281
+ }
282
+ }
283
+
284
+ return '/' + resolved.join('/');
285
+ }
286
+
287
+ /**
288
+ * Get path segments from normalized path
289
+ */
290
+ private getPathSegments(path: string): string[] {
291
+ return this.normalizePath(path).split('/').filter(Boolean);
292
+ }
293
+
294
+ /**
295
+ * Get parent directory path
296
+ */
297
+ private getParentPath(path: string): string {
298
+ const normalized = this.normalizePath(path);
299
+ const lastSlash = normalized.lastIndexOf('/');
300
+ return lastSlash <= 0 ? '/' : normalized.slice(0, lastSlash);
301
+ }
302
+
303
+ /**
304
+ * Get basename from path
305
+ */
306
+ private getBasename(path: string): string {
307
+ const normalized = this.normalizePath(path);
308
+ const lastSlash = normalized.lastIndexOf('/');
309
+ return normalized.slice(lastSlash + 1);
310
+ }
311
+
312
+ /**
313
+ * Get node at path, returns undefined if not found
314
+ */
315
+ private getNode(path: string): FSNode | undefined {
316
+ const segments = this.getPathSegments(path);
317
+ let current = this.root;
318
+
319
+ for (const segment of segments) {
320
+ if (current.type !== 'directory' || !current.children) {
321
+ return undefined;
322
+ }
323
+ const child = current.children.get(segment);
324
+ if (!child) {
325
+ return undefined;
326
+ }
327
+ current = child;
328
+ }
329
+
330
+ return current;
331
+ }
332
+
333
+ /**
334
+ * Get or create directory at path (for mkdir -p behavior)
335
+ */
336
+ private ensureDirectory(path: string): FSNode {
337
+ const segments = this.getPathSegments(path);
338
+ let current = this.root;
339
+
340
+ for (const segment of segments) {
341
+ if (!current.children) {
342
+ current.children = new Map();
343
+ }
344
+
345
+ let child = current.children.get(segment);
346
+ if (!child) {
347
+ child = { type: 'directory', children: new Map() };
348
+ current.children.set(segment, child);
349
+ } else if (child.type !== 'directory') {
350
+ throw new Error(`ENOTDIR: not a directory, '${path}'`);
351
+ }
352
+
353
+ current = child;
354
+ }
355
+
356
+ return current;
357
+ }
358
+
359
+ /**
360
+ * Check if path exists
361
+ */
362
+ existsSync(path: string): boolean {
363
+ return this.getNode(path) !== undefined;
364
+ }
365
+
366
+ /**
367
+ * Get stats for path
368
+ */
369
+ statSync(path: string): Stats {
370
+ const node = this.getNode(path);
371
+ if (!node) {
372
+ throw createNodeError('ENOENT', 'stat', path);
373
+ }
374
+
375
+ const now = Date.now();
376
+ const size = node.type === 'file' ? (node.content?.length || 0) : 0;
377
+
378
+ return {
379
+ isFile: () => node.type === 'file',
380
+ isDirectory: () => node.type === 'directory',
381
+ isSymbolicLink: () => false,
382
+ isBlockDevice: () => false,
383
+ isCharacterDevice: () => false,
384
+ isFIFO: () => false,
385
+ isSocket: () => false,
386
+ size,
387
+ mode: node.type === 'directory' ? 0o755 : 0o644,
388
+ mtime: new Date(now),
389
+ atime: new Date(now),
390
+ ctime: new Date(now),
391
+ birthtime: new Date(now),
392
+ mtimeMs: now,
393
+ atimeMs: now,
394
+ ctimeMs: now,
395
+ birthtimeMs: now,
396
+ nlink: 1,
397
+ uid: 1000,
398
+ gid: 1000,
399
+ dev: 0,
400
+ ino: 0,
401
+ rdev: 0,
402
+ blksize: 4096,
403
+ blocks: Math.ceil(size / 512),
404
+ };
405
+ }
406
+
407
+ /**
408
+ * lstatSync - same as statSync for our virtual FS (no symlinks)
409
+ */
410
+ lstatSync(path: string): Stats {
411
+ return this.statSync(path);
412
+ }
413
+
414
+ /**
415
+ * Read file contents as Uint8Array
416
+ */
417
+ readFileSync(path: string): Uint8Array;
418
+ readFileSync(path: string, encoding: 'utf8' | 'utf-8'): string;
419
+ readFileSync(path: string, encoding?: 'utf8' | 'utf-8'): Uint8Array | string {
420
+ const node = this.getNode(path);
421
+
422
+ if (!node) {
423
+ throw createNodeError('ENOENT', 'open', path);
424
+ }
425
+
426
+ if (node.type !== 'file') {
427
+ throw createNodeError('EISDIR', 'read', path);
428
+ }
429
+
430
+ const content = node.content || new Uint8Array(0);
431
+
432
+ if (encoding === 'utf8' || encoding === 'utf-8') {
433
+ return this.decoder.decode(content);
434
+ }
435
+
436
+ return content;
437
+ }
438
+
439
+ /**
440
+ * Write data to file, creating parent directories as needed
441
+ */
442
+ writeFileSync(path: string, data: string | Uint8Array): void {
443
+ this.writeFileSyncInternal(path, data, true);
444
+ }
445
+
446
+ /**
447
+ * Create directory, optionally with recursive parent creation
448
+ */
449
+ mkdirSync(path: string, options?: { recursive?: boolean }): void {
450
+ const normalized = this.normalizePath(path);
451
+
452
+ if (options?.recursive) {
453
+ this.ensureDirectory(normalized);
454
+ return;
455
+ }
456
+
457
+ const parentPath = this.getParentPath(normalized);
458
+ const basename = this.getBasename(normalized);
459
+
460
+ if (!basename) {
461
+ return; // Root directory already exists
462
+ }
463
+
464
+ const parent = this.getNode(parentPath);
465
+
466
+ if (!parent) {
467
+ throw createNodeError('ENOENT', 'mkdir', parentPath);
468
+ }
469
+
470
+ if (parent.type !== 'directory') {
471
+ throw createNodeError('ENOTDIR', 'mkdir', parentPath);
472
+ }
473
+
474
+ if (parent.children!.has(basename)) {
475
+ throw createNodeError('EEXIST', 'mkdir', path);
476
+ }
477
+
478
+ parent.children!.set(basename, {
479
+ type: 'directory',
480
+ children: new Map(),
481
+ });
482
+ }
483
+
484
+ /**
485
+ * Read directory contents
486
+ */
487
+ readdirSync(path: string): string[] {
488
+ const node = this.getNode(path);
489
+
490
+ if (!node) {
491
+ throw createNodeError('ENOENT', 'scandir', path);
492
+ }
493
+
494
+ if (node.type !== 'directory') {
495
+ throw createNodeError('ENOTDIR', 'scandir', path);
496
+ }
497
+
498
+ return Array.from(node.children!.keys());
499
+ }
500
+
501
+ /**
502
+ * Remove file
503
+ */
504
+ unlinkSync(path: string): void {
505
+ const normalized = this.normalizePath(path);
506
+ const parentPath = this.getParentPath(normalized);
507
+ const basename = this.getBasename(normalized);
508
+
509
+ const parent = this.getNode(parentPath);
510
+
511
+ if (!parent || parent.type !== 'directory') {
512
+ throw createNodeError('ENOENT', 'unlink', path);
513
+ }
514
+
515
+ const node = parent.children!.get(basename);
516
+
517
+ if (!node) {
518
+ throw createNodeError('ENOENT', 'unlink', path);
519
+ }
520
+
521
+ if (node.type !== 'file') {
522
+ throw createNodeError('EISDIR', 'unlink', path);
523
+ }
524
+
525
+ parent.children!.delete(basename);
526
+
527
+ // Notify watchers
528
+ this.notifyWatchers(normalized, 'rename');
529
+ // Emit delete event for worker sync
530
+ this.emit('delete', normalized);
531
+ }
532
+
533
+ /**
534
+ * Remove directory (must be empty)
535
+ */
536
+ rmdirSync(path: string): void {
537
+ const normalized = this.normalizePath(path);
538
+ const parentPath = this.getParentPath(normalized);
539
+ const basename = this.getBasename(normalized);
540
+
541
+ if (!basename) {
542
+ throw new Error(`EPERM: operation not permitted, '${path}'`);
543
+ }
544
+
545
+ const parent = this.getNode(parentPath);
546
+
547
+ if (!parent || parent.type !== 'directory') {
548
+ throw createNodeError('ENOENT', 'rmdir', path);
549
+ }
550
+
551
+ const node = parent.children!.get(basename);
552
+
553
+ if (!node) {
554
+ throw createNodeError('ENOENT', 'rmdir', path);
555
+ }
556
+
557
+ if (node.type !== 'directory') {
558
+ throw createNodeError('ENOTDIR', 'rmdir', path);
559
+ }
560
+
561
+ if (node.children!.size > 0) {
562
+ throw createNodeError('ENOTEMPTY', 'rmdir', path);
563
+ }
564
+
565
+ parent.children!.delete(basename);
566
+ }
567
+
568
+ /**
569
+ * Rename/move file or directory
570
+ */
571
+ renameSync(oldPath: string, newPath: string): void {
572
+ const normalizedOld = this.normalizePath(oldPath);
573
+ const normalizedNew = this.normalizePath(newPath);
574
+
575
+ const oldParentPath = this.getParentPath(normalizedOld);
576
+ const oldBasename = this.getBasename(normalizedOld);
577
+ const newParentPath = this.getParentPath(normalizedNew);
578
+ const newBasename = this.getBasename(normalizedNew);
579
+
580
+ const oldParent = this.getNode(oldParentPath);
581
+
582
+ if (!oldParent || oldParent.type !== 'directory') {
583
+ throw createNodeError('ENOENT', 'rename', oldPath);
584
+ }
585
+
586
+ const node = oldParent.children!.get(oldBasename);
587
+
588
+ if (!node) {
589
+ throw createNodeError('ENOENT', 'rename', oldPath);
590
+ }
591
+
592
+ const newParent = this.ensureDirectory(newParentPath);
593
+
594
+ oldParent.children!.delete(oldBasename);
595
+ newParent.children!.set(newBasename, node);
596
+
597
+ // Notify watchers
598
+ this.notifyWatchers(normalizedOld, 'rename');
599
+ this.notifyWatchers(normalizedNew, 'rename');
600
+ }
601
+
602
+ /**
603
+ * Read file with optional options parameter
604
+ */
605
+ readFile(
606
+ path: string,
607
+ optionsOrCallback?: { encoding?: string } | ((err: Error | null, data?: Uint8Array | string) => void),
608
+ callback?: (err: Error | null, data?: Uint8Array | string) => void
609
+ ): void {
610
+ const actualCallback = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback;
611
+ const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : undefined;
612
+
613
+ try {
614
+ const data = options?.encoding
615
+ ? this.readFileSync(path, options.encoding as 'utf8')
616
+ : this.readFileSync(path);
617
+ if (actualCallback) {
618
+ setTimeout(() => actualCallback(null, data), 0);
619
+ }
620
+ } catch (err) {
621
+ if (actualCallback) {
622
+ setTimeout(() => actualCallback(err as Error), 0);
623
+ }
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Async stat
629
+ */
630
+ stat(path: string, callback: (err: Error | null, stats?: Stats) => void): void {
631
+ try {
632
+ const stats = this.statSync(path);
633
+ setTimeout(() => callback(null, stats), 0);
634
+ } catch (err) {
635
+ setTimeout(() => callback(err as Error), 0);
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Async lstat
641
+ */
642
+ lstat(path: string, callback: (err: Error | null, stats?: Stats) => void): void {
643
+ this.stat(path, callback);
644
+ }
645
+
646
+ /**
647
+ * Async readdir
648
+ */
649
+ readdir(
650
+ path: string,
651
+ optionsOrCallback?: { withFileTypes?: boolean } | ((err: Error | null, files?: string[]) => void),
652
+ callback?: (err: Error | null, files?: string[]) => void
653
+ ): void {
654
+ const actualCallback = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback;
655
+
656
+ try {
657
+ const files = this.readdirSync(path);
658
+ if (actualCallback) {
659
+ setTimeout(() => actualCallback(null, files), 0);
660
+ }
661
+ } catch (err) {
662
+ if (actualCallback) {
663
+ setTimeout(() => actualCallback(err as Error), 0);
664
+ }
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Async realpath
670
+ */
671
+ realpath(path: string, callback: (err: Error | null, resolvedPath?: string) => void): void {
672
+ try {
673
+ const resolved = this.realpathSync(path);
674
+ setTimeout(() => callback(null, resolved), 0);
675
+ } catch (err) {
676
+ setTimeout(() => callback(err as Error), 0);
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Sync realpath - in our VFS, just normalize the path
682
+ */
683
+ realpathSync(path: string): string {
684
+ const normalized = this.normalizePath(path);
685
+ if (!this.existsSync(normalized)) {
686
+ throw createNodeError('ENOENT', 'realpath', path);
687
+ }
688
+ return normalized;
689
+ }
690
+
691
+ /**
692
+ * Watch for file changes
693
+ */
694
+ watch(
695
+ filename: string,
696
+ optionsOrListener?: { persistent?: boolean; recursive?: boolean; encoding?: string } | WatchListener,
697
+ listener?: WatchListener
698
+ ): FSWatcher {
699
+ const normalized = this.normalizePath(filename);
700
+
701
+ // Parse arguments
702
+ let options: { persistent?: boolean; recursive?: boolean } = {};
703
+ let actualListener: WatchListener | undefined;
704
+
705
+ if (typeof optionsOrListener === 'function') {
706
+ actualListener = optionsOrListener;
707
+ } else if (optionsOrListener) {
708
+ options = optionsOrListener;
709
+ actualListener = listener;
710
+ } else {
711
+ actualListener = listener;
712
+ }
713
+
714
+ // Create watcher entry
715
+ const entry: WatcherEntry = {
716
+ listener: actualListener || (() => {}),
717
+ recursive: options.recursive || false,
718
+ closed: false,
719
+ };
720
+
721
+ // Add to watchers map
722
+ if (!this.watchers.has(normalized)) {
723
+ this.watchers.set(normalized, new Set());
724
+ }
725
+ this.watchers.get(normalized)!.add(entry);
726
+
727
+ // Return FSWatcher interface
728
+ const watcher: FSWatcher = {
729
+ close: () => {
730
+ entry.closed = true;
731
+ const watcherSet = this.watchers.get(normalized);
732
+ if (watcherSet) {
733
+ watcherSet.delete(entry);
734
+ if (watcherSet.size === 0) {
735
+ this.watchers.delete(normalized);
736
+ }
737
+ }
738
+ },
739
+ ref: () => watcher,
740
+ unref: () => watcher,
741
+ };
742
+
743
+ return watcher;
744
+ }
745
+
746
+ /**
747
+ * Notify watchers of file changes
748
+ */
749
+ private notifyWatchers(path: string, eventType: WatchEventType): void {
750
+ const normalized = this.normalizePath(path);
751
+ const basename = this.getBasename(normalized);
752
+
753
+ // Check direct watchers on this file
754
+ const directWatchers = this.watchers.get(normalized);
755
+ if (directWatchers) {
756
+ for (const entry of directWatchers) {
757
+ if (!entry.closed) {
758
+ try {
759
+ entry.listener(eventType, basename);
760
+ } catch (err) {
761
+ console.error('Error in file watcher:', err);
762
+ }
763
+ }
764
+ }
765
+ }
766
+
767
+ // Check parent directory watchers (recursive and non-recursive)
768
+ let currentPath = this.getParentPath(normalized);
769
+ let relativePath = basename;
770
+
771
+ while (currentPath) {
772
+ const parentWatchers = this.watchers.get(currentPath);
773
+ if (parentWatchers) {
774
+ for (const entry of parentWatchers) {
775
+ if (!entry.closed) {
776
+ // Non-recursive watchers only get notified for direct children
777
+ const isDirectChild = this.getParentPath(normalized) === currentPath;
778
+ if (entry.recursive || isDirectChild) {
779
+ try {
780
+ entry.listener(eventType, relativePath);
781
+ } catch (err) {
782
+ console.error('Error in file watcher:', err);
783
+ }
784
+ }
785
+ }
786
+ }
787
+ }
788
+
789
+ if (currentPath === '/') break;
790
+ relativePath = this.getBasename(currentPath) + '/' + relativePath;
791
+ currentPath = this.getParentPath(currentPath);
792
+ }
793
+ }
794
+
795
+ /**
796
+ * Access check - in our VFS, always succeeds if file exists
797
+ */
798
+ accessSync(path: string, mode?: number): void {
799
+ if (!this.existsSync(path)) {
800
+ throw createNodeError('ENOENT', 'access', path);
801
+ }
802
+ }
803
+
804
+ /**
805
+ * Async access
806
+ */
807
+ access(path: string, modeOrCallback?: number | ((err: Error | null) => void), callback?: (err: Error | null) => void): void {
808
+ const actualCallback = typeof modeOrCallback === 'function' ? modeOrCallback : callback;
809
+ try {
810
+ this.accessSync(path);
811
+ if (actualCallback) setTimeout(() => actualCallback(null), 0);
812
+ } catch (err) {
813
+ if (actualCallback) setTimeout(() => actualCallback(err as Error), 0);
814
+ }
815
+ }
816
+
817
+ /**
818
+ * Copy file
819
+ */
820
+ copyFileSync(src: string, dest: string): void {
821
+ const content = this.readFileSync(src);
822
+ this.writeFileSync(dest, content);
823
+ }
824
+
825
+ /**
826
+ * Create read stream - simplified implementation
827
+ */
828
+ createReadStream(path: string): {
829
+ on: (event: string, cb: (...args: unknown[]) => void) => void;
830
+ pipe: (dest: unknown) => unknown;
831
+ } {
832
+ const self = this;
833
+ const listeners: Record<string, ((...args: unknown[]) => void)[]> = {};
834
+
835
+ const stream = {
836
+ on(event: string, cb: (...args: unknown[]) => void) {
837
+ if (!listeners[event]) listeners[event] = [];
838
+ listeners[event].push(cb);
839
+ return stream;
840
+ },
841
+ pipe(dest: unknown) {
842
+ return dest;
843
+ },
844
+ };
845
+
846
+ // Emit data asynchronously
847
+ setTimeout(() => {
848
+ try {
849
+ const data = self.readFileSync(path);
850
+ listeners['data']?.forEach((cb) => cb(data));
851
+ listeners['end']?.forEach((cb) => cb());
852
+ } catch (err) {
853
+ listeners['error']?.forEach((cb) => cb(err));
854
+ }
855
+ }, 0);
856
+
857
+ return stream;
858
+ }
859
+
860
+ /**
861
+ * Create write stream - simplified implementation
862
+ */
863
+ createWriteStream(path: string): {
864
+ write: (data: string | Uint8Array) => boolean;
865
+ end: (data?: string | Uint8Array) => void;
866
+ on: (event: string, cb: (...args: unknown[]) => void) => void;
867
+ } {
868
+ const self = this;
869
+ const chunks: Uint8Array[] = [];
870
+ const listeners: Record<string, ((...args: unknown[]) => void)[]> = {};
871
+ const encoder = new TextEncoder();
872
+
873
+ return {
874
+ write(data: string | Uint8Array): boolean {
875
+ const chunk = typeof data === 'string' ? encoder.encode(data) : data;
876
+ chunks.push(chunk);
877
+ return true;
878
+ },
879
+ end(data?: string | Uint8Array): void {
880
+ if (data) {
881
+ const chunk = typeof data === 'string' ? encoder.encode(data) : data;
882
+ chunks.push(chunk);
883
+ }
884
+ // Combine chunks
885
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
886
+ const combined = new Uint8Array(totalLength);
887
+ let offset = 0;
888
+ for (const chunk of chunks) {
889
+ combined.set(chunk, offset);
890
+ offset += chunk.length;
891
+ }
892
+ self.writeFileSync(path, combined);
893
+ listeners['finish']?.forEach((cb) => cb());
894
+ listeners['close']?.forEach((cb) => cb());
895
+ },
896
+ on(event: string, cb: (...args: unknown[]) => void) {
897
+ if (!listeners[event]) listeners[event] = [];
898
+ listeners[event].push(cb);
899
+ return this;
900
+ },
901
+ };
902
+ }
903
+ }