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.
- package/LICENSE +21 -0
- package/README.md +731 -0
- package/dist/__sw__.js +394 -0
- package/dist/ai-chatbot-demo-entry.d.ts +6 -0
- package/dist/ai-chatbot-demo-entry.d.ts.map +1 -0
- package/dist/ai-chatbot-demo.d.ts +42 -0
- package/dist/ai-chatbot-demo.d.ts.map +1 -0
- package/dist/assets/runtime-worker-D9x_Ddwz.js +60543 -0
- package/dist/assets/runtime-worker-D9x_Ddwz.js.map +1 -0
- package/dist/convex-app-demo-entry.d.ts +6 -0
- package/dist/convex-app-demo-entry.d.ts.map +1 -0
- package/dist/convex-app-demo.d.ts +68 -0
- package/dist/convex-app-demo.d.ts.map +1 -0
- package/dist/cors-proxy.d.ts +46 -0
- package/dist/cors-proxy.d.ts.map +1 -0
- package/dist/create-runtime.d.ts +42 -0
- package/dist/create-runtime.d.ts.map +1 -0
- package/dist/demo.d.ts +6 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/dev-server.d.ts +97 -0
- package/dist/dev-server.d.ts.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +202 -0
- package/dist/frameworks/next-dev-server.d.ts.map +1 -0
- package/dist/frameworks/vite-dev-server.d.ts +85 -0
- package/dist/frameworks/vite-dev-server.d.ts.map +1 -0
- package/dist/index.cjs +14965 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +14867 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next-demo.d.ts +49 -0
- package/dist/next-demo.d.ts.map +1 -0
- package/dist/npm/index.d.ts +71 -0
- package/dist/npm/index.d.ts.map +1 -0
- package/dist/npm/registry.d.ts +66 -0
- package/dist/npm/registry.d.ts.map +1 -0
- package/dist/npm/resolver.d.ts +52 -0
- package/dist/npm/resolver.d.ts.map +1 -0
- package/dist/npm/tarball.d.ts +29 -0
- package/dist/npm/tarball.d.ts.map +1 -0
- package/dist/runtime-interface.d.ts +90 -0
- package/dist/runtime-interface.d.ts.map +1 -0
- package/dist/runtime.d.ts +103 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/sandbox-helpers.d.ts +43 -0
- package/dist/sandbox-helpers.d.ts.map +1 -0
- package/dist/sandbox-runtime.d.ts +65 -0
- package/dist/sandbox-runtime.d.ts.map +1 -0
- package/dist/server-bridge.d.ts +89 -0
- package/dist/server-bridge.d.ts.map +1 -0
- package/dist/shims/assert.d.ts +51 -0
- package/dist/shims/assert.d.ts.map +1 -0
- package/dist/shims/async_hooks.d.ts +37 -0
- package/dist/shims/async_hooks.d.ts.map +1 -0
- package/dist/shims/buffer.d.ts +20 -0
- package/dist/shims/buffer.d.ts.map +1 -0
- package/dist/shims/child_process-browser.d.ts +92 -0
- package/dist/shims/child_process-browser.d.ts.map +1 -0
- package/dist/shims/child_process.d.ts +93 -0
- package/dist/shims/child_process.d.ts.map +1 -0
- package/dist/shims/chokidar.d.ts +55 -0
- package/dist/shims/chokidar.d.ts.map +1 -0
- package/dist/shims/cluster.d.ts +52 -0
- package/dist/shims/cluster.d.ts.map +1 -0
- package/dist/shims/crypto.d.ts +122 -0
- package/dist/shims/crypto.d.ts.map +1 -0
- package/dist/shims/dgram.d.ts +34 -0
- package/dist/shims/dgram.d.ts.map +1 -0
- package/dist/shims/diagnostics_channel.d.ts +80 -0
- package/dist/shims/diagnostics_channel.d.ts.map +1 -0
- package/dist/shims/dns.d.ts +87 -0
- package/dist/shims/dns.d.ts.map +1 -0
- package/dist/shims/domain.d.ts +25 -0
- package/dist/shims/domain.d.ts.map +1 -0
- package/dist/shims/esbuild.d.ts +105 -0
- package/dist/shims/esbuild.d.ts.map +1 -0
- package/dist/shims/events.d.ts +37 -0
- package/dist/shims/events.d.ts.map +1 -0
- package/dist/shims/fs.d.ts +115 -0
- package/dist/shims/fs.d.ts.map +1 -0
- package/dist/shims/fsevents.d.ts +67 -0
- package/dist/shims/fsevents.d.ts.map +1 -0
- package/dist/shims/http.d.ts +217 -0
- package/dist/shims/http.d.ts.map +1 -0
- package/dist/shims/http2.d.ts +81 -0
- package/dist/shims/http2.d.ts.map +1 -0
- package/dist/shims/https.d.ts +36 -0
- package/dist/shims/https.d.ts.map +1 -0
- package/dist/shims/inspector.d.ts +25 -0
- package/dist/shims/inspector.d.ts.map +1 -0
- package/dist/shims/module.d.ts +22 -0
- package/dist/shims/module.d.ts.map +1 -0
- package/dist/shims/net.d.ts +100 -0
- package/dist/shims/net.d.ts.map +1 -0
- package/dist/shims/os.d.ts +159 -0
- package/dist/shims/os.d.ts.map +1 -0
- package/dist/shims/path.d.ts +72 -0
- package/dist/shims/path.d.ts.map +1 -0
- package/dist/shims/perf_hooks.d.ts +50 -0
- package/dist/shims/perf_hooks.d.ts.map +1 -0
- package/dist/shims/process.d.ts +93 -0
- package/dist/shims/process.d.ts.map +1 -0
- package/dist/shims/querystring.d.ts +23 -0
- package/dist/shims/querystring.d.ts.map +1 -0
- package/dist/shims/readdirp.d.ts +52 -0
- package/dist/shims/readdirp.d.ts.map +1 -0
- package/dist/shims/readline.d.ts +62 -0
- package/dist/shims/readline.d.ts.map +1 -0
- package/dist/shims/rollup.d.ts +34 -0
- package/dist/shims/rollup.d.ts.map +1 -0
- package/dist/shims/sentry.d.ts +163 -0
- package/dist/shims/sentry.d.ts.map +1 -0
- package/dist/shims/stream.d.ts +181 -0
- package/dist/shims/stream.d.ts.map +1 -0
- package/dist/shims/tls.d.ts +53 -0
- package/dist/shims/tls.d.ts.map +1 -0
- package/dist/shims/tty.d.ts +30 -0
- package/dist/shims/tty.d.ts.map +1 -0
- package/dist/shims/url.d.ts +64 -0
- package/dist/shims/url.d.ts.map +1 -0
- package/dist/shims/util.d.ts +106 -0
- package/dist/shims/util.d.ts.map +1 -0
- package/dist/shims/v8.d.ts +73 -0
- package/dist/shims/v8.d.ts.map +1 -0
- package/dist/shims/vfs-adapter.d.ts +126 -0
- package/dist/shims/vfs-adapter.d.ts.map +1 -0
- package/dist/shims/vm.d.ts +45 -0
- package/dist/shims/vm.d.ts.map +1 -0
- package/dist/shims/worker_threads.d.ts +66 -0
- package/dist/shims/worker_threads.d.ts.map +1 -0
- package/dist/shims/ws.d.ts +66 -0
- package/dist/shims/ws.d.ts.map +1 -0
- package/dist/shims/zlib.d.ts +161 -0
- package/dist/shims/zlib.d.ts.map +1 -0
- package/dist/transform.d.ts +24 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts +226 -0
- package/dist/virtual-fs.d.ts.map +1 -0
- package/dist/vite-demo.d.ts +35 -0
- package/dist/vite-demo.d.ts.map +1 -0
- package/dist/vite-sw.js +132 -0
- package/dist/worker/runtime-worker.d.ts +8 -0
- package/dist/worker/runtime-worker.d.ts.map +1 -0
- package/dist/worker-runtime.d.ts +50 -0
- package/dist/worker-runtime.d.ts.map +1 -0
- package/package.json +85 -0
- package/src/ai-chatbot-demo-entry.ts +244 -0
- package/src/ai-chatbot-demo.ts +509 -0
- package/src/convex-app-demo-entry.ts +1107 -0
- package/src/convex-app-demo.ts +1316 -0
- package/src/cors-proxy.ts +81 -0
- package/src/create-runtime.ts +147 -0
- package/src/demo.ts +304 -0
- package/src/dev-server.ts +274 -0
- package/src/frameworks/next-dev-server.ts +2224 -0
- package/src/frameworks/vite-dev-server.ts +702 -0
- package/src/index.ts +101 -0
- package/src/next-demo.ts +1784 -0
- package/src/npm/index.ts +347 -0
- package/src/npm/registry.ts +152 -0
- package/src/npm/resolver.ts +385 -0
- package/src/npm/tarball.ts +209 -0
- package/src/runtime-interface.ts +103 -0
- package/src/runtime.ts +1046 -0
- package/src/sandbox-helpers.ts +173 -0
- package/src/sandbox-runtime.ts +252 -0
- package/src/server-bridge.ts +426 -0
- package/src/shims/assert.ts +664 -0
- package/src/shims/async_hooks.ts +86 -0
- package/src/shims/buffer.ts +75 -0
- package/src/shims/child_process-browser.ts +217 -0
- package/src/shims/child_process.ts +463 -0
- package/src/shims/chokidar.ts +313 -0
- package/src/shims/cluster.ts +67 -0
- package/src/shims/crypto.ts +830 -0
- package/src/shims/dgram.ts +47 -0
- package/src/shims/diagnostics_channel.ts +196 -0
- package/src/shims/dns.ts +172 -0
- package/src/shims/domain.ts +58 -0
- package/src/shims/esbuild.ts +805 -0
- package/src/shims/events.ts +195 -0
- package/src/shims/fs.ts +803 -0
- package/src/shims/fsevents.ts +63 -0
- package/src/shims/http.ts +904 -0
- package/src/shims/http2.ts +96 -0
- package/src/shims/https.ts +86 -0
- package/src/shims/inspector.ts +30 -0
- package/src/shims/module.ts +82 -0
- package/src/shims/net.ts +359 -0
- package/src/shims/os.ts +195 -0
- package/src/shims/path.ts +199 -0
- package/src/shims/perf_hooks.ts +92 -0
- package/src/shims/process.ts +346 -0
- package/src/shims/querystring.ts +97 -0
- package/src/shims/readdirp.ts +228 -0
- package/src/shims/readline.ts +110 -0
- package/src/shims/rollup.ts +80 -0
- package/src/shims/sentry.ts +133 -0
- package/src/shims/stream.ts +1126 -0
- package/src/shims/tls.ts +95 -0
- package/src/shims/tty.ts +64 -0
- package/src/shims/url.ts +171 -0
- package/src/shims/util.ts +312 -0
- package/src/shims/v8.ts +113 -0
- package/src/shims/vfs-adapter.ts +402 -0
- package/src/shims/vm.ts +83 -0
- package/src/shims/worker_threads.ts +111 -0
- package/src/shims/ws.ts +382 -0
- package/src/shims/zlib.ts +289 -0
- package/src/transform.ts +313 -0
- package/src/types/external.d.ts +67 -0
- package/src/virtual-fs.ts +903 -0
- package/src/vite-demo.ts +577 -0
- package/src/worker/runtime-worker.ts +128 -0
- 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
|
+
}
|