@run0/jiki 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/dist/browser-bundle.d.ts +40 -0
- package/dist/builtins.d.ts +22 -0
- package/dist/code-transform.d.ts +7 -0
- package/dist/config/cdn.d.ts +13 -0
- package/dist/container.d.ts +101 -0
- package/dist/dev-server.d.ts +69 -0
- package/dist/errors.d.ts +19 -0
- package/dist/frameworks/code-transforms.d.ts +32 -0
- package/dist/frameworks/next-api-handler.d.ts +72 -0
- package/dist/frameworks/next-dev-server.d.ts +141 -0
- package/dist/frameworks/next-html-generator.d.ts +36 -0
- package/dist/frameworks/next-route-resolver.d.ts +19 -0
- package/dist/frameworks/next-shims.d.ts +78 -0
- package/dist/frameworks/remix-dev-server.d.ts +47 -0
- package/dist/frameworks/sveltekit-dev-server.d.ts +43 -0
- package/dist/frameworks/vite-dev-server.d.ts +50 -0
- package/dist/fs-errors.d.ts +36 -0
- package/dist/index.cjs +14916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.mjs +14898 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kernel.d.ts +48 -0
- package/dist/memfs.d.ts +144 -0
- package/dist/metrics.d.ts +78 -0
- package/dist/module-resolver.d.ts +60 -0
- package/dist/network-interceptor.d.ts +71 -0
- package/dist/npm/cache.d.ts +76 -0
- package/dist/npm/index.d.ts +60 -0
- package/dist/npm/lockfile-reader.d.ts +32 -0
- package/dist/npm/pnpm.d.ts +18 -0
- package/dist/npm/registry.d.ts +45 -0
- package/dist/npm/resolver.d.ts +39 -0
- package/dist/npm/sync-installer.d.ts +18 -0
- package/dist/npm/tarball.d.ts +4 -0
- package/dist/npm/workspaces.d.ts +46 -0
- package/dist/persistence.d.ts +94 -0
- package/dist/plugin.d.ts +156 -0
- package/dist/polyfills/assert.d.ts +30 -0
- package/dist/polyfills/child_process.d.ts +116 -0
- package/dist/polyfills/chokidar.d.ts +18 -0
- package/dist/polyfills/crypto.d.ts +49 -0
- package/dist/polyfills/events.d.ts +28 -0
- package/dist/polyfills/fs.d.ts +82 -0
- package/dist/polyfills/http.d.ts +147 -0
- package/dist/polyfills/module.d.ts +29 -0
- package/dist/polyfills/net.d.ts +53 -0
- package/dist/polyfills/os.d.ts +91 -0
- package/dist/polyfills/path.d.ts +96 -0
- package/dist/polyfills/perf_hooks.d.ts +21 -0
- package/dist/polyfills/process.d.ts +99 -0
- package/dist/polyfills/querystring.d.ts +15 -0
- package/dist/polyfills/readdirp.d.ts +18 -0
- package/dist/polyfills/readline.d.ts +32 -0
- package/dist/polyfills/stream.d.ts +106 -0
- package/dist/polyfills/stubs.d.ts +737 -0
- package/dist/polyfills/tty.d.ts +25 -0
- package/dist/polyfills/url.d.ts +41 -0
- package/dist/polyfills/util.d.ts +61 -0
- package/dist/polyfills/v8.d.ts +43 -0
- package/dist/polyfills/vm.d.ts +76 -0
- package/dist/polyfills/worker-threads.d.ts +77 -0
- package/dist/polyfills/ws.d.ts +32 -0
- package/dist/polyfills/zlib.d.ts +87 -0
- package/dist/runtime-helpers.d.ts +4 -0
- package/dist/runtime-interface.d.ts +39 -0
- package/dist/sandbox.d.ts +69 -0
- package/dist/server-bridge.d.ts +55 -0
- package/dist/shell-commands.d.ts +2 -0
- package/dist/shell.d.ts +101 -0
- package/dist/transpiler.d.ts +47 -0
- package/dist/type-checker.d.ts +57 -0
- package/dist/types/package-json.d.ts +17 -0
- package/dist/utils/binary-encoding.d.ts +4 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/safe-path.d.ts +6 -0
- package/dist/worker-runtime.d.ts +34 -0
- package/package.json +59 -0
- package/src/browser-bundle.ts +498 -0
- package/src/builtins.ts +222 -0
- package/src/code-transform.ts +183 -0
- package/src/config/cdn.ts +17 -0
- package/src/container.ts +343 -0
- package/src/dev-server.ts +322 -0
- package/src/errors.ts +604 -0
- package/src/frameworks/code-transforms.ts +667 -0
- package/src/frameworks/next-api-handler.ts +366 -0
- package/src/frameworks/next-dev-server.ts +1252 -0
- package/src/frameworks/next-html-generator.ts +585 -0
- package/src/frameworks/next-route-resolver.ts +521 -0
- package/src/frameworks/next-shims.ts +1084 -0
- package/src/frameworks/remix-dev-server.ts +163 -0
- package/src/frameworks/sveltekit-dev-server.ts +197 -0
- package/src/frameworks/vite-dev-server.ts +370 -0
- package/src/fs-errors.ts +118 -0
- package/src/index.ts +188 -0
- package/src/kernel.ts +381 -0
- package/src/memfs.ts +1006 -0
- package/src/metrics.ts +140 -0
- package/src/module-resolver.ts +511 -0
- package/src/network-interceptor.ts +143 -0
- package/src/npm/cache.ts +172 -0
- package/src/npm/index.ts +377 -0
- package/src/npm/lockfile-reader.ts +105 -0
- package/src/npm/pnpm.ts +108 -0
- package/src/npm/registry.ts +120 -0
- package/src/npm/resolver.ts +339 -0
- package/src/npm/sync-installer.ts +217 -0
- package/src/npm/tarball.ts +136 -0
- package/src/npm/workspaces.ts +255 -0
- package/src/persistence.ts +235 -0
- package/src/plugin.ts +293 -0
- package/src/polyfills/assert.ts +164 -0
- package/src/polyfills/child_process.ts +535 -0
- package/src/polyfills/chokidar.ts +52 -0
- package/src/polyfills/crypto.ts +433 -0
- package/src/polyfills/events.ts +178 -0
- package/src/polyfills/fs.ts +297 -0
- package/src/polyfills/http.ts +478 -0
- package/src/polyfills/module.ts +97 -0
- package/src/polyfills/net.ts +123 -0
- package/src/polyfills/os.ts +108 -0
- package/src/polyfills/path.ts +169 -0
- package/src/polyfills/perf_hooks.ts +30 -0
- package/src/polyfills/process.ts +349 -0
- package/src/polyfills/querystring.ts +66 -0
- package/src/polyfills/readdirp.ts +72 -0
- package/src/polyfills/readline.ts +80 -0
- package/src/polyfills/stream.ts +610 -0
- package/src/polyfills/stubs.ts +600 -0
- package/src/polyfills/tty.ts +43 -0
- package/src/polyfills/url.ts +97 -0
- package/src/polyfills/util.ts +173 -0
- package/src/polyfills/v8.ts +62 -0
- package/src/polyfills/vm.ts +111 -0
- package/src/polyfills/worker-threads.ts +189 -0
- package/src/polyfills/ws.ts +73 -0
- package/src/polyfills/zlib.ts +244 -0
- package/src/runtime-helpers.ts +83 -0
- package/src/runtime-interface.ts +46 -0
- package/src/sandbox.ts +178 -0
- package/src/server-bridge.ts +473 -0
- package/src/service-worker.ts +153 -0
- package/src/shell-commands.ts +708 -0
- package/src/shell.ts +795 -0
- package/src/transpiler.ts +282 -0
- package/src/type-checker.ts +241 -0
- package/src/types/package-json.ts +17 -0
- package/src/utils/binary-encoding.ts +38 -0
- package/src/utils/hash.ts +24 -0
- package/src/utils/safe-path.ts +38 -0
- package/src/worker-runtime.ts +42 -0
package/src/memfs.ts
ADDED
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import type { VFSSnapshot, VFSFileEntry } from "./runtime-interface";
|
|
2
|
+
import { uint8ToBase64, base64ToUint8 } from "./utils/binary-encoding";
|
|
3
|
+
import {
|
|
4
|
+
createNodeError,
|
|
5
|
+
buildStats,
|
|
6
|
+
type NodeError,
|
|
7
|
+
type Stats,
|
|
8
|
+
type ErrorCode,
|
|
9
|
+
} from "./fs-errors";
|
|
10
|
+
import {
|
|
11
|
+
nodeToEntry,
|
|
12
|
+
type PersistenceAdapter,
|
|
13
|
+
type PersistedEntry,
|
|
14
|
+
} from "./persistence";
|
|
15
|
+
import type { SandboxGuard } from "./sandbox";
|
|
16
|
+
|
|
17
|
+
export { createNodeError, type NodeError, type Stats };
|
|
18
|
+
|
|
19
|
+
let _nodeInoSeq = 1;
|
|
20
|
+
|
|
21
|
+
export interface FSNode {
|
|
22
|
+
type: "file" | "directory" | "symlink";
|
|
23
|
+
content?: Uint8Array;
|
|
24
|
+
children?: Map<string, FSNode>;
|
|
25
|
+
target?: string;
|
|
26
|
+
mtime: number;
|
|
27
|
+
ino: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ChangeListener = (path: string, content: string) => void;
|
|
31
|
+
type DeleteListener = (path: string) => void;
|
|
32
|
+
type AnyListener = ChangeListener | DeleteListener;
|
|
33
|
+
|
|
34
|
+
export type WatchEventType = "change" | "rename";
|
|
35
|
+
export type WatchListener = (
|
|
36
|
+
eventType: WatchEventType,
|
|
37
|
+
filename: string | null,
|
|
38
|
+
) => void;
|
|
39
|
+
|
|
40
|
+
export interface FSWatcher {
|
|
41
|
+
close(): void;
|
|
42
|
+
ref(): this;
|
|
43
|
+
unref(): this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface WatcherEntry {
|
|
47
|
+
listener: WatchListener;
|
|
48
|
+
recursive: boolean;
|
|
49
|
+
closed: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---- Path helpers (consolidated into an object) ----
|
|
53
|
+
|
|
54
|
+
const pops = {
|
|
55
|
+
normalize(p: string): string {
|
|
56
|
+
if (!p.startsWith("/")) p = "/" + p;
|
|
57
|
+
const parts = p.split("/").filter(Boolean);
|
|
58
|
+
const out: string[] = [];
|
|
59
|
+
for (const seg of parts) {
|
|
60
|
+
if (seg === "..") out.pop();
|
|
61
|
+
else if (seg !== ".") out.push(seg);
|
|
62
|
+
}
|
|
63
|
+
return "/" + out.join("/");
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
segments(p: string): string[] {
|
|
67
|
+
return pops.normalize(p).split("/").filter(Boolean);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
parent(p: string): string {
|
|
71
|
+
const norm = pops.normalize(p);
|
|
72
|
+
const idx = norm.lastIndexOf("/");
|
|
73
|
+
return idx <= 0 ? "/" : norm.slice(0, idx);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
name(p: string): string {
|
|
77
|
+
const norm = pops.normalize(p);
|
|
78
|
+
return norm.slice(norm.lastIndexOf("/") + 1);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ---- Snapshot serialization (standalone functions) ----
|
|
83
|
+
|
|
84
|
+
function serializeTree(path: string, node: FSNode, acc: VFSFileEntry[]): void {
|
|
85
|
+
if (node.type === "file") {
|
|
86
|
+
acc.push({
|
|
87
|
+
path,
|
|
88
|
+
type: "file",
|
|
89
|
+
content: node.content?.length ? uint8ToBase64(node.content) : "",
|
|
90
|
+
});
|
|
91
|
+
} else if (node.type === "symlink") {
|
|
92
|
+
acc.push({ path, type: "symlink", target: node.target });
|
|
93
|
+
} else if (node.type === "directory") {
|
|
94
|
+
acc.push({ path, type: "directory" });
|
|
95
|
+
if (node.children) {
|
|
96
|
+
for (const [childName, childNode] of node.children) {
|
|
97
|
+
const childPath =
|
|
98
|
+
path === "/" ? `/${childName}` : `${path}/${childName}`;
|
|
99
|
+
serializeTree(childPath, childNode, acc);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function deserializeInto(vfs: MemFS, snapshot: VFSSnapshot): void {
|
|
106
|
+
const ordered = snapshot.files
|
|
107
|
+
.map((e, i) => ({ e, depth: e.path.split("/").length, i }))
|
|
108
|
+
.sort((a, b) => a.depth - b.depth || a.i - b.i)
|
|
109
|
+
.map(x => x.e);
|
|
110
|
+
|
|
111
|
+
for (const entry of ordered) {
|
|
112
|
+
if (entry.path === "/") continue;
|
|
113
|
+
switch (entry.type) {
|
|
114
|
+
case "directory":
|
|
115
|
+
vfs.mkdirSync(entry.path, { recursive: true });
|
|
116
|
+
break;
|
|
117
|
+
case "symlink":
|
|
118
|
+
if (entry.target) vfs.symlinkSync(entry.target, entry.path);
|
|
119
|
+
break;
|
|
120
|
+
case "file": {
|
|
121
|
+
const dir = entry.path.substring(0, entry.path.lastIndexOf("/")) || "/";
|
|
122
|
+
if (dir !== "/" && !vfs.existsSync(dir))
|
|
123
|
+
vfs.mkdirSync(dir, { recursive: true });
|
|
124
|
+
const bytes = entry.content
|
|
125
|
+
? base64ToUint8(entry.content)
|
|
126
|
+
: new Uint8Array(0);
|
|
127
|
+
vfs.putFile(entry.path, bytes, false);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Options for MemFS construction. */
|
|
135
|
+
export interface MemFSOptions {
|
|
136
|
+
/** Optional persistence adapter. When provided, filesystem mutations are
|
|
137
|
+
* automatically persisted to the backend. */
|
|
138
|
+
persistence?: PersistenceAdapter;
|
|
139
|
+
/** Optional sandbox guard. When provided, all write operations are
|
|
140
|
+
* checked against resource limits and path restrictions. */
|
|
141
|
+
sandbox?: SandboxGuard;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class MemFS {
|
|
145
|
+
private root: FSNode;
|
|
146
|
+
private enc = new TextEncoder();
|
|
147
|
+
private dec = new TextDecoder();
|
|
148
|
+
private watchers = new Map<string, Set<WatcherEntry>>();
|
|
149
|
+
private eventSubs = new Map<string, Set<AnyListener>>();
|
|
150
|
+
private nodeIndex = new Map<string, FSNode>();
|
|
151
|
+
private symlinkLimit = 20;
|
|
152
|
+
private fdCounter = 3; // 0,1,2 reserved for stdin/stdout/stderr
|
|
153
|
+
private fdMap = new Map<number, { path: string; flags: string }>();
|
|
154
|
+
/** Persistence adapter (if configured). */
|
|
155
|
+
private persistence?: PersistenceAdapter;
|
|
156
|
+
/** Sandbox guard (if configured). */
|
|
157
|
+
private sandboxGuard?: SandboxGuard;
|
|
158
|
+
|
|
159
|
+
constructor(options?: MemFSOptions) {
|
|
160
|
+
this.root = {
|
|
161
|
+
type: "directory",
|
|
162
|
+
children: new Map(),
|
|
163
|
+
mtime: Date.now(),
|
|
164
|
+
ino: _nodeInoSeq++,
|
|
165
|
+
};
|
|
166
|
+
this.nodeIndex.set("/", this.root);
|
|
167
|
+
this.persistence = options?.persistence;
|
|
168
|
+
this.sandboxGuard = options?.sandbox;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Rehydrate the VFS from the persistence adapter.
|
|
173
|
+
* Call this once during initialisation (e.g. in `Container.init()`) to
|
|
174
|
+
* restore previously persisted state.
|
|
175
|
+
*/
|
|
176
|
+
async hydrate(): Promise<number> {
|
|
177
|
+
if (!this.persistence) return 0;
|
|
178
|
+
const entries = await this.persistence.loadAll();
|
|
179
|
+
// Sort by path depth so parents are created before children.
|
|
180
|
+
entries.sort((a, b) => a.path.split("/").length - b.path.split("/").length);
|
|
181
|
+
|
|
182
|
+
let count = 0;
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (entry.path === "/") continue;
|
|
185
|
+
try {
|
|
186
|
+
switch (entry.type) {
|
|
187
|
+
case "directory":
|
|
188
|
+
if (!this.existsSync(entry.path)) {
|
|
189
|
+
this.mkdirSync(entry.path, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
case "symlink":
|
|
193
|
+
if (entry.target && !this.existsSync(entry.path)) {
|
|
194
|
+
this.symlinkSync(entry.target, entry.path);
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
case "file": {
|
|
198
|
+
const dir =
|
|
199
|
+
entry.path.substring(0, entry.path.lastIndexOf("/")) || "/";
|
|
200
|
+
if (dir !== "/" && !this.existsSync(dir)) {
|
|
201
|
+
this.mkdirSync(dir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
const bytes = entry.content ?? new Uint8Array(0);
|
|
204
|
+
// Use putFile with notify=false to avoid triggering watchers
|
|
205
|
+
// and persistence hooks during hydration.
|
|
206
|
+
this.putFile(entry.path, bytes, false);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
count++;
|
|
211
|
+
} catch {
|
|
212
|
+
// Skip entries that fail to hydrate (e.g. corrupted data).
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return count;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Flush any pending persistence writes immediately.
|
|
220
|
+
* Useful before page unload or when you need to guarantee data is saved.
|
|
221
|
+
*/
|
|
222
|
+
async flushPersistence(): Promise<void> {
|
|
223
|
+
if (this.persistence) await this.persistence.flush();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Queue a node for persistence (fire-and-forget). */
|
|
227
|
+
private persist(path: string, node: FSNode): void {
|
|
228
|
+
if (this.persistence) this.persistence.save(nodeToEntry(path, node));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Queue a path deletion for persistence (fire-and-forget). */
|
|
232
|
+
private unpersist(path: string): void {
|
|
233
|
+
if (this.persistence) this.persistence.delete(path);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Queue deletion of a path and all its descendants. */
|
|
237
|
+
private unpersistTree(path: string): void {
|
|
238
|
+
if (!this.persistence) return;
|
|
239
|
+
this.persistence.delete(path);
|
|
240
|
+
// Also delete descendants based on nodeIndex keys.
|
|
241
|
+
const prefix = path === "/" ? "/" : path + "/";
|
|
242
|
+
for (const key of this.nodeIndex.keys()) {
|
|
243
|
+
if (key.startsWith(prefix)) this.persistence.delete(key);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- Event subscription ----
|
|
248
|
+
|
|
249
|
+
on(event: "change", listener: ChangeListener): this;
|
|
250
|
+
on(event: "delete", listener: DeleteListener): this;
|
|
251
|
+
on(event: string, listener: AnyListener): this {
|
|
252
|
+
if (!this.eventSubs.has(event)) this.eventSubs.set(event, new Set());
|
|
253
|
+
this.eventSubs.get(event)!.add(listener);
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
off(event: "change", listener: ChangeListener): this;
|
|
258
|
+
off(event: "delete", listener: DeleteListener): this;
|
|
259
|
+
off(event: string, listener: AnyListener): this {
|
|
260
|
+
this.eventSubs.get(event)?.delete(listener);
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private fire(event: "change", path: string, content: string): void;
|
|
265
|
+
private fire(event: "delete", path: string): void;
|
|
266
|
+
private fire(event: string, ...args: unknown[]): void {
|
|
267
|
+
const subs = this.eventSubs.get(event);
|
|
268
|
+
if (!subs) return;
|
|
269
|
+
for (const fn of subs) {
|
|
270
|
+
try {
|
|
271
|
+
(fn as (...a: unknown[]) => void)(...args);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error("VFS event listener error:", err);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---- Snapshot ----
|
|
279
|
+
|
|
280
|
+
toSnapshot(): VFSSnapshot {
|
|
281
|
+
const entries: VFSFileEntry[] = [];
|
|
282
|
+
serializeTree("/", this.root, entries);
|
|
283
|
+
return { files: entries };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
static fromSnapshot(snapshot: VFSSnapshot): MemFS {
|
|
287
|
+
const fs = new MemFS();
|
|
288
|
+
deserializeInto(fs, snapshot);
|
|
289
|
+
return fs;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---- Export ----
|
|
293
|
+
|
|
294
|
+
export(
|
|
295
|
+
path: string,
|
|
296
|
+
_options?: { format?: "json" },
|
|
297
|
+
): Record<string, unknown> {
|
|
298
|
+
const node = this.followLinks(path);
|
|
299
|
+
if (!node || node.type !== "directory")
|
|
300
|
+
throw createNodeError("ENOTDIR", "export", path);
|
|
301
|
+
return this.treeToJson(path, node);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private treeToJson(path: string, node: FSNode): Record<string, unknown> {
|
|
305
|
+
const out: Record<string, unknown> = {};
|
|
306
|
+
if (!node.children) return out;
|
|
307
|
+
for (const [name, child] of node.children) {
|
|
308
|
+
switch (child.type) {
|
|
309
|
+
case "file":
|
|
310
|
+
out[name] = {
|
|
311
|
+
file: {
|
|
312
|
+
contents: this.dec.decode(child.content || new Uint8Array(0)),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
break;
|
|
316
|
+
case "directory":
|
|
317
|
+
out[name] = { directory: this.treeToJson(`${path}/${name}`, child) };
|
|
318
|
+
break;
|
|
319
|
+
case "symlink":
|
|
320
|
+
out[name] = { file: { symlink: child.target } };
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---- Node traversal ----
|
|
328
|
+
|
|
329
|
+
private lookupDirect(path: string): FSNode | undefined {
|
|
330
|
+
const norm = pops.normalize(path);
|
|
331
|
+
const cached = this.nodeIndex.get(norm);
|
|
332
|
+
if (cached) return cached;
|
|
333
|
+
const segs = pops.segments(path);
|
|
334
|
+
let cur = this.root;
|
|
335
|
+
for (const seg of segs) {
|
|
336
|
+
if (cur.type !== "directory" || !cur.children) return undefined;
|
|
337
|
+
const next = cur.children.get(seg);
|
|
338
|
+
if (!next) return undefined;
|
|
339
|
+
cur = next;
|
|
340
|
+
}
|
|
341
|
+
this.nodeIndex.set(norm, cur);
|
|
342
|
+
return cur;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private followLinks(path: string, depth = 0): FSNode | undefined {
|
|
346
|
+
if (depth >= this.symlinkLimit)
|
|
347
|
+
throw createNodeError("ELOOP", "stat", path);
|
|
348
|
+
|
|
349
|
+
// Fast path: direct lookup works when no intermediate symlinks exist
|
|
350
|
+
const direct = this.lookupDirect(path);
|
|
351
|
+
if (direct) {
|
|
352
|
+
if (direct.type === "symlink" && direct.target) {
|
|
353
|
+
const dest = direct.target.startsWith("/")
|
|
354
|
+
? direct.target
|
|
355
|
+
: pops.normalize(pops.parent(path) + "/" + direct.target);
|
|
356
|
+
return this.followLinks(dest, depth + 1);
|
|
357
|
+
}
|
|
358
|
+
return direct;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Slow path: walk segment by segment, resolving symlinks at each component
|
|
362
|
+
const segs = pops.segments(path);
|
|
363
|
+
let cur = this.root;
|
|
364
|
+
for (let i = 0; i < segs.length; i++) {
|
|
365
|
+
if (cur.type !== "directory" || !cur.children) return undefined;
|
|
366
|
+
const next = cur.children.get(segs[i]);
|
|
367
|
+
if (!next) return undefined;
|
|
368
|
+
if (next.type === "symlink" && next.target) {
|
|
369
|
+
const builtSoFar = "/" + segs.slice(0, i + 1).join("/");
|
|
370
|
+
const dest = next.target.startsWith("/")
|
|
371
|
+
? next.target
|
|
372
|
+
: pops.normalize(pops.parent(builtSoFar) + "/" + next.target);
|
|
373
|
+
const remaining = segs.slice(i + 1);
|
|
374
|
+
const full =
|
|
375
|
+
remaining.length > 0 ? dest + "/" + remaining.join("/") : dest;
|
|
376
|
+
return this.followLinks(full, depth + 1);
|
|
377
|
+
}
|
|
378
|
+
cur = next;
|
|
379
|
+
}
|
|
380
|
+
return cur;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private ensureDir(path: string): FSNode {
|
|
384
|
+
const segs = pops.segments(path);
|
|
385
|
+
let cur = this.root;
|
|
386
|
+
let built = "";
|
|
387
|
+
for (const seg of segs) {
|
|
388
|
+
built += "/" + seg;
|
|
389
|
+
if (!cur.children) cur.children = new Map();
|
|
390
|
+
let child = cur.children.get(seg);
|
|
391
|
+
if (!child) {
|
|
392
|
+
child = {
|
|
393
|
+
type: "directory",
|
|
394
|
+
children: new Map(),
|
|
395
|
+
mtime: Date.now(),
|
|
396
|
+
ino: _nodeInoSeq++,
|
|
397
|
+
};
|
|
398
|
+
cur.children.set(seg, child);
|
|
399
|
+
this.nodeIndex.set(built, child);
|
|
400
|
+
} else if (child.type === "symlink" && child.target) {
|
|
401
|
+
const resolved = this.followLinks(built);
|
|
402
|
+
if (resolved && resolved.type === "directory") {
|
|
403
|
+
child = resolved;
|
|
404
|
+
} else {
|
|
405
|
+
throw createNodeError("ENOTDIR", "mkdir", path);
|
|
406
|
+
}
|
|
407
|
+
} else if (child.type !== "directory") {
|
|
408
|
+
throw createNodeError("ENOTDIR", "mkdir", path);
|
|
409
|
+
}
|
|
410
|
+
cur = child;
|
|
411
|
+
}
|
|
412
|
+
return cur;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private dropIndex(path: string): void {
|
|
416
|
+
const prefix = pops.normalize(path);
|
|
417
|
+
const fullPrefix = prefix === "/" ? "/" : prefix + "/";
|
|
418
|
+
this.nodeIndex.delete(prefix);
|
|
419
|
+
for (const key of this.nodeIndex.keys()) {
|
|
420
|
+
if (key === prefix || key.startsWith(fullPrefix))
|
|
421
|
+
this.nodeIndex.delete(key);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---- Core FS operations ----
|
|
426
|
+
|
|
427
|
+
existsSync(path: string): boolean {
|
|
428
|
+
return this.followLinks(path) !== undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
statSync(path: string): Stats {
|
|
432
|
+
const node = this.followLinks(path);
|
|
433
|
+
if (!node) throw createNodeError("ENOENT", "stat", path);
|
|
434
|
+
return this.toStats(node);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
lstatSync(path: string): Stats {
|
|
438
|
+
const node = this.lookupDirect(path);
|
|
439
|
+
if (!node) throw createNodeError("ENOENT", "lstat", path);
|
|
440
|
+
return this.toStats(node);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private toStats(node: FSNode): Stats {
|
|
444
|
+
const sz = node.type === "file" ? node.content?.length || 0 : 0;
|
|
445
|
+
return buildStats(node.type, sz, node.mtime, node.ino);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
readFileSync(path: string): Uint8Array;
|
|
449
|
+
readFileSync(path: string, encoding: "utf8" | "utf-8"): string;
|
|
450
|
+
readFileSync(path: string, encoding?: "utf8" | "utf-8"): Uint8Array | string {
|
|
451
|
+
const node = this.followLinks(path);
|
|
452
|
+
if (!node) throw createNodeError("ENOENT", "open", path);
|
|
453
|
+
if (node.type !== "file") throw createNodeError("EISDIR", "read", path);
|
|
454
|
+
const raw = node.content || new Uint8Array(0);
|
|
455
|
+
return encoding === "utf8" || encoding === "utf-8"
|
|
456
|
+
? this.dec.decode(raw)
|
|
457
|
+
: raw;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
writeFileSync(path: string, data: string | Uint8Array): void {
|
|
461
|
+
this.putFile(path, data, true);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
putFile(path: string, data: string | Uint8Array, notify: boolean): void {
|
|
465
|
+
const norm = pops.normalize(path);
|
|
466
|
+
const dir = pops.parent(norm);
|
|
467
|
+
const base = pops.name(norm);
|
|
468
|
+
if (!base) throw createNodeError("EISDIR", "write", path);
|
|
469
|
+
|
|
470
|
+
const bytes = typeof data === "string" ? this.enc.encode(data) : data;
|
|
471
|
+
|
|
472
|
+
// Enforce sandbox at the VFS level so all callers are checked.
|
|
473
|
+
if (this.sandboxGuard?.isActive && notify) {
|
|
474
|
+
this.sandboxGuard.checkWrite(norm, bytes.length);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const parent = this.ensureDir(dir);
|
|
478
|
+
const existed = parent.children!.has(base);
|
|
479
|
+
|
|
480
|
+
const fileNode: FSNode = {
|
|
481
|
+
type: "file",
|
|
482
|
+
content: bytes,
|
|
483
|
+
mtime: Date.now(),
|
|
484
|
+
ino: _nodeInoSeq++,
|
|
485
|
+
};
|
|
486
|
+
parent.children!.set(base, fileNode);
|
|
487
|
+
this.nodeIndex.set(norm, fileNode);
|
|
488
|
+
|
|
489
|
+
if (notify) {
|
|
490
|
+
if (this.sandboxGuard?.isActive && !existed) {
|
|
491
|
+
this.sandboxGuard.trackWrite(bytes.length);
|
|
492
|
+
}
|
|
493
|
+
this.signalWatchers(norm, existed ? "change" : "rename");
|
|
494
|
+
this.fire(
|
|
495
|
+
"change",
|
|
496
|
+
norm,
|
|
497
|
+
typeof data === "string" ? data : this.dec.decode(data),
|
|
498
|
+
);
|
|
499
|
+
this.persist(norm, fileNode);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---- Symlink ----
|
|
504
|
+
|
|
505
|
+
symlinkSync(target: string, linkPath: string): void {
|
|
506
|
+
const norm = pops.normalize(linkPath);
|
|
507
|
+
const dir = pops.parent(norm);
|
|
508
|
+
const base = pops.name(norm);
|
|
509
|
+
if (!base) throw createNodeError("EEXIST", "symlink", linkPath);
|
|
510
|
+
|
|
511
|
+
const parent = this.ensureDir(dir);
|
|
512
|
+
if (parent.children!.has(base))
|
|
513
|
+
throw createNodeError("EEXIST", "symlink", linkPath);
|
|
514
|
+
const node: FSNode = {
|
|
515
|
+
type: "symlink",
|
|
516
|
+
target,
|
|
517
|
+
mtime: Date.now(),
|
|
518
|
+
ino: _nodeInoSeq++,
|
|
519
|
+
};
|
|
520
|
+
parent.children!.set(base, node);
|
|
521
|
+
this.nodeIndex.set(norm, node);
|
|
522
|
+
this.persist(norm, node);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
readlinkSync(path: string): string {
|
|
526
|
+
const node = this.lookupDirect(path);
|
|
527
|
+
if (!node) throw createNodeError("ENOENT", "readlink", path);
|
|
528
|
+
if (node.type !== "symlink")
|
|
529
|
+
throw createNodeError("EINVAL", "readlink", path);
|
|
530
|
+
return node.target!;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
mkdirSync(path: string, options?: { recursive?: boolean }): void {
|
|
534
|
+
const norm = pops.normalize(path);
|
|
535
|
+
if (options?.recursive) {
|
|
536
|
+
this.ensureDir(norm);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const dir = pops.parent(norm);
|
|
540
|
+
const base = pops.name(norm);
|
|
541
|
+
if (!base) {
|
|
542
|
+
// Normalized to "/" — root already exists, nothing to do
|
|
543
|
+
if (norm === "/") return;
|
|
544
|
+
// Empty filename from malformed path
|
|
545
|
+
throw createNodeError("ENOENT", "mkdir", path);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const parent = this.followLinks(dir);
|
|
549
|
+
if (!parent) throw createNodeError("ENOENT", "mkdir", dir);
|
|
550
|
+
if (parent.type !== "directory")
|
|
551
|
+
throw createNodeError("ENOTDIR", "mkdir", dir);
|
|
552
|
+
if (parent.children!.has(base))
|
|
553
|
+
throw createNodeError("EEXIST", "mkdir", path);
|
|
554
|
+
const newDir: FSNode = {
|
|
555
|
+
type: "directory",
|
|
556
|
+
children: new Map(),
|
|
557
|
+
mtime: Date.now(),
|
|
558
|
+
ino: _nodeInoSeq++,
|
|
559
|
+
};
|
|
560
|
+
parent.children!.set(base, newDir);
|
|
561
|
+
this.nodeIndex.set(norm, newDir);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
readdirSync(path: string): string[];
|
|
565
|
+
readdirSync(
|
|
566
|
+
path: string,
|
|
567
|
+
options: { withFileTypes: true },
|
|
568
|
+
): {
|
|
569
|
+
name: string;
|
|
570
|
+
isFile(): boolean;
|
|
571
|
+
isDirectory(): boolean;
|
|
572
|
+
isSymbolicLink(): boolean;
|
|
573
|
+
}[];
|
|
574
|
+
readdirSync(path: string, options?: { withFileTypes?: boolean }): unknown {
|
|
575
|
+
const node = this.followLinks(path);
|
|
576
|
+
if (!node) throw createNodeError("ENOENT", "scandir", path);
|
|
577
|
+
if (node.type !== "directory")
|
|
578
|
+
throw createNodeError("ENOTDIR", "scandir", path);
|
|
579
|
+
const names = Array.from(node.children!.keys());
|
|
580
|
+
if (!options?.withFileTypes) return names;
|
|
581
|
+
return names.map(n => {
|
|
582
|
+
const child = node.children!.get(n)!;
|
|
583
|
+
return {
|
|
584
|
+
name: n,
|
|
585
|
+
isFile: () => child.type === "file",
|
|
586
|
+
isDirectory: () => child.type === "directory",
|
|
587
|
+
isSymbolicLink: () => child.type === "symlink",
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
unlinkSync(path: string): void {
|
|
593
|
+
const norm = pops.normalize(path);
|
|
594
|
+
const dir = pops.parent(norm);
|
|
595
|
+
const base = pops.name(norm);
|
|
596
|
+
const parent = this.followLinks(dir);
|
|
597
|
+
if (!parent || parent.type !== "directory")
|
|
598
|
+
throw createNodeError("ENOENT", "unlink", path);
|
|
599
|
+
const target = parent.children!.get(base);
|
|
600
|
+
if (!target) throw createNodeError("ENOENT", "unlink", path);
|
|
601
|
+
if (target.type === "directory")
|
|
602
|
+
throw createNodeError("EISDIR", "unlink", path);
|
|
603
|
+
const targetSize =
|
|
604
|
+
target.type === "file" ? (target.content?.length ?? 0) : 0;
|
|
605
|
+
parent.children!.delete(base);
|
|
606
|
+
this.nodeIndex.delete(norm);
|
|
607
|
+
this.signalWatchers(norm, "rename");
|
|
608
|
+
this.fire("delete", norm);
|
|
609
|
+
this.unpersist(norm);
|
|
610
|
+
if (this.sandboxGuard?.isActive) this.sandboxGuard.trackDelete(targetSize);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
rmdirSync(path: string, options?: { recursive?: boolean }): void {
|
|
614
|
+
const norm = pops.normalize(path);
|
|
615
|
+
const dir = pops.parent(norm);
|
|
616
|
+
const base = pops.name(norm);
|
|
617
|
+
if (!base) throw createNodeError("EPERM", "rmdir", path);
|
|
618
|
+
|
|
619
|
+
const parent = this.followLinks(dir);
|
|
620
|
+
if (!parent || parent.type !== "directory")
|
|
621
|
+
throw createNodeError("ENOENT", "rmdir", path);
|
|
622
|
+
const target = parent.children!.get(base);
|
|
623
|
+
if (!target) throw createNodeError("ENOENT", "rmdir", path);
|
|
624
|
+
if (target.type !== "directory")
|
|
625
|
+
throw createNodeError("ENOTDIR", "rmdir", path);
|
|
626
|
+
|
|
627
|
+
if (options?.recursive) {
|
|
628
|
+
this.unpersistTree(norm);
|
|
629
|
+
parent.children!.delete(base);
|
|
630
|
+
this.dropIndex(norm);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (target.children!.size > 0)
|
|
634
|
+
throw createNodeError("ENOTEMPTY", "rmdir", path);
|
|
635
|
+
parent.children!.delete(base);
|
|
636
|
+
this.nodeIndex.delete(norm);
|
|
637
|
+
this.unpersist(norm);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
rmSync(
|
|
641
|
+
path: string,
|
|
642
|
+
options?: { recursive?: boolean; force?: boolean },
|
|
643
|
+
): void {
|
|
644
|
+
const norm = pops.normalize(path);
|
|
645
|
+
try {
|
|
646
|
+
const node = this.followLinks(norm);
|
|
647
|
+
if (!node) {
|
|
648
|
+
if (options?.force) return;
|
|
649
|
+
throw createNodeError("ENOENT", "rm", path);
|
|
650
|
+
}
|
|
651
|
+
if (node.type === "directory") {
|
|
652
|
+
if (!options?.recursive) throw createNodeError("EISDIR", "rm", path);
|
|
653
|
+
this.rmdirSync(path, { recursive: true });
|
|
654
|
+
} else {
|
|
655
|
+
this.unlinkSync(path);
|
|
656
|
+
}
|
|
657
|
+
} catch (e) {
|
|
658
|
+
if (options?.force && (e as NodeError).code === "ENOENT") return;
|
|
659
|
+
throw e;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
renameSync(oldPath: string, newPath: string): void {
|
|
664
|
+
const normOld = pops.normalize(oldPath);
|
|
665
|
+
const normNew = pops.normalize(newPath);
|
|
666
|
+
const oldDir = pops.parent(normOld);
|
|
667
|
+
const oldBase = pops.name(normOld);
|
|
668
|
+
const newDir = pops.parent(normNew);
|
|
669
|
+
const newBase = pops.name(normNew);
|
|
670
|
+
|
|
671
|
+
const oldParent = this.followLinks(oldDir);
|
|
672
|
+
if (!oldParent || oldParent.type !== "directory")
|
|
673
|
+
throw createNodeError("ENOENT", "rename", oldPath);
|
|
674
|
+
const node = oldParent.children!.get(oldBase);
|
|
675
|
+
if (!node) throw createNodeError("ENOENT", "rename", oldPath);
|
|
676
|
+
|
|
677
|
+
const newParent = this.ensureDir(newDir);
|
|
678
|
+
oldParent.children!.delete(oldBase);
|
|
679
|
+
newParent.children!.set(newBase, node);
|
|
680
|
+
this.dropIndex(normOld);
|
|
681
|
+
this.nodeIndex.set(normNew, node);
|
|
682
|
+
this.signalWatchers(normOld, "rename");
|
|
683
|
+
this.signalWatchers(normNew, "rename");
|
|
684
|
+
this.unpersist(normOld);
|
|
685
|
+
this.persist(normNew, node);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
copyFileSync(src: string, dest: string): void {
|
|
689
|
+
const raw = this.readFileSync(src);
|
|
690
|
+
this.writeFileSync(dest, new Uint8Array(raw));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
utimesSync(path: string, _atime: number | Date, mtime: number | Date): void {
|
|
694
|
+
const node = this.followLinks(path);
|
|
695
|
+
if (!node) throw createNodeError("ENOENT", "utimes", path);
|
|
696
|
+
node.mtime = typeof mtime === "number" ? mtime : mtime.getTime();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
accessSync(path: string, _mode?: number): void {
|
|
700
|
+
if (!this.existsSync(path)) throw createNodeError("ENOENT", "access", path);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
realpathSync(path: string): string {
|
|
704
|
+
const norm = pops.normalize(path);
|
|
705
|
+
if (!this.existsSync(norm))
|
|
706
|
+
throw createNodeError("ENOENT", "realpath", path);
|
|
707
|
+
return norm;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ---- File descriptor APIs ----
|
|
711
|
+
|
|
712
|
+
openSync(path: string, flags: string | number = "r"): number {
|
|
713
|
+
const norm = pops.normalize(path);
|
|
714
|
+
const flagStr = typeof flags === "number" ? "r" : flags;
|
|
715
|
+
if (flagStr === "r" || flagStr === "r+") {
|
|
716
|
+
// File must exist for reading
|
|
717
|
+
this.readFileSync(norm); // throws ENOENT if missing
|
|
718
|
+
}
|
|
719
|
+
if (flagStr === "w" || flagStr === "w+" || flagStr === "a") {
|
|
720
|
+
// Create file if it doesn't exist
|
|
721
|
+
if (!this.existsSync(norm)) {
|
|
722
|
+
this.writeFileSync(norm, "");
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const fd = this.fdCounter++;
|
|
726
|
+
this.fdMap.set(fd, { path: norm, flags: flagStr });
|
|
727
|
+
return fd;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
closeSync(fd: number): void {
|
|
731
|
+
if (!this.fdMap.has(fd)) {
|
|
732
|
+
throw new Error(`EBADF: bad file descriptor, close`);
|
|
733
|
+
}
|
|
734
|
+
this.fdMap.delete(fd);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
readSync(
|
|
738
|
+
fd: number,
|
|
739
|
+
buffer: Uint8Array,
|
|
740
|
+
offset: number,
|
|
741
|
+
length: number,
|
|
742
|
+
position: number | null,
|
|
743
|
+
): number {
|
|
744
|
+
const entry = this.fdMap.get(fd);
|
|
745
|
+
if (!entry) throw new Error(`EBADF: bad file descriptor, read`);
|
|
746
|
+
const content = this.readFileSync(entry.path) as Uint8Array;
|
|
747
|
+
const pos = position ?? 0;
|
|
748
|
+
const end = Math.min(pos + length, content.length);
|
|
749
|
+
const bytesRead = end - pos;
|
|
750
|
+
buffer.set(content.subarray(pos, end), offset);
|
|
751
|
+
return bytesRead;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
writeSync(
|
|
755
|
+
fd: number,
|
|
756
|
+
buffer: Uint8Array,
|
|
757
|
+
offset: number,
|
|
758
|
+
length: number,
|
|
759
|
+
position: number | null,
|
|
760
|
+
): number {
|
|
761
|
+
const entry = this.fdMap.get(fd);
|
|
762
|
+
if (!entry) throw new Error(`EBADF: bad file descriptor, write`);
|
|
763
|
+
const existing = this.existsSync(entry.path)
|
|
764
|
+
? (this.readFileSync(entry.path) as Uint8Array)
|
|
765
|
+
: new Uint8Array(0);
|
|
766
|
+
const pos = position ?? existing.length;
|
|
767
|
+
const newLen = Math.max(existing.length, pos + length);
|
|
768
|
+
const result = new Uint8Array(newLen);
|
|
769
|
+
result.set(existing);
|
|
770
|
+
result.set(buffer.subarray(offset, offset + length), pos);
|
|
771
|
+
this.writeFileSync(entry.path, result);
|
|
772
|
+
return length;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
fstatSync(fd: number): Stats {
|
|
776
|
+
const entry = this.fdMap.get(fd);
|
|
777
|
+
if (!entry) throw new Error(`EBADF: bad file descriptor, fstat`);
|
|
778
|
+
return this.statSync(entry.path);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ---- Async wrappers ----
|
|
782
|
+
|
|
783
|
+
readFile(
|
|
784
|
+
path: string,
|
|
785
|
+
optionsOrCb?:
|
|
786
|
+
| { encoding?: string }
|
|
787
|
+
| ((err: Error | null, data?: Uint8Array | string) => void),
|
|
788
|
+
cb?: (err: Error | null, data?: Uint8Array | string) => void,
|
|
789
|
+
): void {
|
|
790
|
+
const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb;
|
|
791
|
+
const opts = typeof optionsOrCb === "object" ? optionsOrCb : undefined;
|
|
792
|
+
try {
|
|
793
|
+
const data = opts?.encoding
|
|
794
|
+
? this.readFileSync(path, opts.encoding as "utf8")
|
|
795
|
+
: this.readFileSync(path);
|
|
796
|
+
if (callback) setTimeout(() => callback(null, data), 0);
|
|
797
|
+
} catch (err) {
|
|
798
|
+
if (callback) setTimeout(() => callback(err as Error), 0);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
stat(path: string, cb: (err: Error | null, stats?: Stats) => void): void {
|
|
803
|
+
try {
|
|
804
|
+
cb(null, this.statSync(path));
|
|
805
|
+
} catch (err) {
|
|
806
|
+
cb(err as Error);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
lstat(path: string, cb: (err: Error | null, stats?: Stats) => void): void {
|
|
811
|
+
try {
|
|
812
|
+
cb(null, this.lstatSync(path));
|
|
813
|
+
} catch (err) {
|
|
814
|
+
cb(err as Error);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
readdir(
|
|
819
|
+
path: string,
|
|
820
|
+
optionsOrCb?:
|
|
821
|
+
| { withFileTypes?: boolean }
|
|
822
|
+
| ((err: Error | null, files?: string[]) => void),
|
|
823
|
+
cb?: (err: Error | null, files?: unknown) => void,
|
|
824
|
+
): void {
|
|
825
|
+
const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb;
|
|
826
|
+
const opts = typeof optionsOrCb === "object" ? optionsOrCb : undefined;
|
|
827
|
+
try {
|
|
828
|
+
const files = opts?.withFileTypes
|
|
829
|
+
? this.readdirSync(path, { withFileTypes: true })
|
|
830
|
+
: this.readdirSync(path);
|
|
831
|
+
if (callback) setTimeout(() => callback(null, files as string[]), 0);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
if (callback) setTimeout(() => callback(err as Error), 0);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
realpath(
|
|
838
|
+
path: string,
|
|
839
|
+
cb: (err: Error | null, resolved?: string) => void,
|
|
840
|
+
): void {
|
|
841
|
+
try {
|
|
842
|
+
cb(null, this.realpathSync(path));
|
|
843
|
+
} catch (err) {
|
|
844
|
+
cb(err as Error);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
access(
|
|
849
|
+
path: string,
|
|
850
|
+
modeOrCb?: number | ((err: Error | null) => void),
|
|
851
|
+
cb?: (err: Error | null) => void,
|
|
852
|
+
): void {
|
|
853
|
+
const callback = typeof modeOrCb === "function" ? modeOrCb : cb;
|
|
854
|
+
try {
|
|
855
|
+
this.accessSync(path);
|
|
856
|
+
if (callback) setTimeout(() => callback(null), 0);
|
|
857
|
+
} catch (err) {
|
|
858
|
+
if (callback) setTimeout(() => callback(err as Error), 0);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ---- File watching ----
|
|
863
|
+
|
|
864
|
+
watch(
|
|
865
|
+
filename: string,
|
|
866
|
+
optionsOrListener?:
|
|
867
|
+
| { persistent?: boolean; recursive?: boolean; encoding?: string }
|
|
868
|
+
| WatchListener,
|
|
869
|
+
listener?: WatchListener,
|
|
870
|
+
): FSWatcher {
|
|
871
|
+
const norm = pops.normalize(filename);
|
|
872
|
+
let opts: { recursive?: boolean } = {};
|
|
873
|
+
let actualListener: WatchListener | undefined;
|
|
874
|
+
|
|
875
|
+
if (typeof optionsOrListener === "function") {
|
|
876
|
+
actualListener = optionsOrListener;
|
|
877
|
+
} else if (optionsOrListener) {
|
|
878
|
+
opts = optionsOrListener;
|
|
879
|
+
actualListener = listener;
|
|
880
|
+
} else {
|
|
881
|
+
actualListener = listener;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const entry: WatcherEntry = {
|
|
885
|
+
listener: actualListener || (() => {}),
|
|
886
|
+
recursive: opts.recursive || false,
|
|
887
|
+
closed: false,
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
if (!this.watchers.has(norm)) this.watchers.set(norm, new Set());
|
|
891
|
+
this.watchers.get(norm)!.add(entry);
|
|
892
|
+
|
|
893
|
+
const handle: FSWatcher = {
|
|
894
|
+
close: () => {
|
|
895
|
+
entry.closed = true;
|
|
896
|
+
const bucket = this.watchers.get(norm);
|
|
897
|
+
if (bucket) {
|
|
898
|
+
bucket.delete(entry);
|
|
899
|
+
if (bucket.size === 0) this.watchers.delete(norm);
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
ref: () => handle,
|
|
903
|
+
unref: () => handle,
|
|
904
|
+
};
|
|
905
|
+
return handle;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private signalWatchers(changedPath: string, kind: WatchEventType): void {
|
|
909
|
+
const norm = pops.normalize(changedPath);
|
|
910
|
+
const leaf = pops.name(norm);
|
|
911
|
+
|
|
912
|
+
for (const [watchPath, bucket] of this.watchers) {
|
|
913
|
+
if (watchPath === norm) {
|
|
914
|
+
for (const w of bucket) {
|
|
915
|
+
if (!w.closed) {
|
|
916
|
+
try {
|
|
917
|
+
w.listener(kind, leaf);
|
|
918
|
+
} catch {}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (!norm.startsWith(watchPath + "/") && watchPath !== "/") continue;
|
|
925
|
+
|
|
926
|
+
const isDirectChild = pops.parent(norm) === watchPath;
|
|
927
|
+
const rel =
|
|
928
|
+
norm === watchPath
|
|
929
|
+
? ""
|
|
930
|
+
: norm.slice((watchPath === "/" ? 0 : watchPath.length) + 1);
|
|
931
|
+
|
|
932
|
+
for (const w of bucket) {
|
|
933
|
+
if (w.closed) continue;
|
|
934
|
+
if (w.recursive || isDirectChild) {
|
|
935
|
+
try {
|
|
936
|
+
w.listener(kind, rel);
|
|
937
|
+
} catch {}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// ---- Stream helpers ----
|
|
944
|
+
|
|
945
|
+
createReadStream(path: string): {
|
|
946
|
+
on: (event: string, cb: (...args: unknown[]) => void) => unknown;
|
|
947
|
+
pipe: (dest: unknown) => unknown;
|
|
948
|
+
} {
|
|
949
|
+
const self = this;
|
|
950
|
+
const subs: Record<string, ((...args: unknown[]) => void)[]> = {};
|
|
951
|
+
const stream = {
|
|
952
|
+
on(ev: string, cb: (...args: unknown[]) => void) {
|
|
953
|
+
(subs[ev] ??= []).push(cb);
|
|
954
|
+
return stream;
|
|
955
|
+
},
|
|
956
|
+
pipe(dest: unknown) {
|
|
957
|
+
return dest;
|
|
958
|
+
},
|
|
959
|
+
};
|
|
960
|
+
setTimeout(() => {
|
|
961
|
+
try {
|
|
962
|
+
const data = self.readFileSync(path);
|
|
963
|
+
subs["data"]?.forEach(cb => cb(data));
|
|
964
|
+
subs["end"]?.forEach(cb => cb());
|
|
965
|
+
} catch (err) {
|
|
966
|
+
subs["error"]?.forEach(cb => cb(err));
|
|
967
|
+
}
|
|
968
|
+
}, 0);
|
|
969
|
+
return stream;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
createWriteStream(path: string): {
|
|
973
|
+
write: (data: string | Uint8Array) => boolean;
|
|
974
|
+
end: (data?: string | Uint8Array) => void;
|
|
975
|
+
on: (event: string, cb: (...args: unknown[]) => void) => unknown;
|
|
976
|
+
} {
|
|
977
|
+
const self = this;
|
|
978
|
+
const parts: Uint8Array[] = [];
|
|
979
|
+
const subs: Record<string, ((...args: unknown[]) => void)[]> = {};
|
|
980
|
+
const encoder = new TextEncoder();
|
|
981
|
+
return {
|
|
982
|
+
write(data: string | Uint8Array): boolean {
|
|
983
|
+
parts.push(typeof data === "string" ? encoder.encode(data) : data);
|
|
984
|
+
return true;
|
|
985
|
+
},
|
|
986
|
+
end(data?: string | Uint8Array): void {
|
|
987
|
+
if (data)
|
|
988
|
+
parts.push(typeof data === "string" ? encoder.encode(data) : data);
|
|
989
|
+
const total = parts.reduce((s, c) => s + c.length, 0);
|
|
990
|
+
const merged = new Uint8Array(total);
|
|
991
|
+
let off = 0;
|
|
992
|
+
for (const chunk of parts) {
|
|
993
|
+
merged.set(chunk, off);
|
|
994
|
+
off += chunk.length;
|
|
995
|
+
}
|
|
996
|
+
self.writeFileSync(path, merged);
|
|
997
|
+
subs["finish"]?.forEach(cb => cb());
|
|
998
|
+
subs["close"]?.forEach(cb => cb());
|
|
999
|
+
},
|
|
1000
|
+
on(ev: string, cb: (...args: unknown[]) => void) {
|
|
1001
|
+
(subs[ev] ??= []).push(cb);
|
|
1002
|
+
return this;
|
|
1003
|
+
},
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|