@scelar/nodepod 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +43 -0
- package/README.md +240 -0
- package/dist/child_process-BJOMsZje.js +8233 -0
- package/dist/child_process-BJOMsZje.js.map +1 -0
- package/dist/child_process-Cj8vOcuc.cjs +7434 -0
- package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
- package/dist/index-Cb1Cgdnd.js +35308 -0
- package/dist/index-Cb1Cgdnd.js.map +1 -0
- package/dist/index-DsMGS-xc.cjs +37195 -0
- package/dist/index-DsMGS-xc.cjs.map +1 -0
- package/dist/index.cjs +65 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +59 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +95 -0
- package/src/__tests__/smoke.test.ts +11 -0
- package/src/constants/cdn-urls.ts +18 -0
- package/src/constants/config.ts +236 -0
- package/src/cross-origin.ts +26 -0
- package/src/engine-factory.ts +176 -0
- package/src/engine-types.ts +56 -0
- package/src/helpers/byte-encoding.ts +39 -0
- package/src/helpers/digest.ts +9 -0
- package/src/helpers/event-loop.ts +96 -0
- package/src/helpers/wasm-cache.ts +133 -0
- package/src/iframe-sandbox.ts +141 -0
- package/src/index.ts +192 -0
- package/src/isolation-helpers.ts +148 -0
- package/src/memory-volume.ts +941 -0
- package/src/module-transformer.ts +368 -0
- package/src/packages/archive-extractor.ts +248 -0
- package/src/packages/browser-bundler.ts +284 -0
- package/src/packages/installer.ts +396 -0
- package/src/packages/registry-client.ts +131 -0
- package/src/packages/version-resolver.ts +411 -0
- package/src/polyfills/assert.ts +384 -0
- package/src/polyfills/async_hooks.ts +144 -0
- package/src/polyfills/buffer.ts +628 -0
- package/src/polyfills/child_process.ts +2288 -0
- package/src/polyfills/chokidar.ts +336 -0
- package/src/polyfills/cluster.ts +106 -0
- package/src/polyfills/console.ts +136 -0
- package/src/polyfills/constants.ts +123 -0
- package/src/polyfills/crypto.ts +885 -0
- package/src/polyfills/dgram.ts +87 -0
- package/src/polyfills/diagnostics_channel.ts +76 -0
- package/src/polyfills/dns.ts +134 -0
- package/src/polyfills/domain.ts +68 -0
- package/src/polyfills/esbuild.ts +854 -0
- package/src/polyfills/events.ts +276 -0
- package/src/polyfills/fs.ts +2888 -0
- package/src/polyfills/fsevents.ts +79 -0
- package/src/polyfills/http.ts +1449 -0
- package/src/polyfills/http2.ts +199 -0
- package/src/polyfills/https.ts +76 -0
- package/src/polyfills/inspector.ts +62 -0
- package/src/polyfills/lightningcss.ts +105 -0
- package/src/polyfills/module.ts +191 -0
- package/src/polyfills/net.ts +353 -0
- package/src/polyfills/os.ts +238 -0
- package/src/polyfills/path.ts +206 -0
- package/src/polyfills/perf_hooks.ts +102 -0
- package/src/polyfills/process.ts +690 -0
- package/src/polyfills/punycode.ts +159 -0
- package/src/polyfills/querystring.ts +93 -0
- package/src/polyfills/quic.ts +118 -0
- package/src/polyfills/readdirp.ts +229 -0
- package/src/polyfills/readline.ts +692 -0
- package/src/polyfills/repl.ts +134 -0
- package/src/polyfills/rollup.ts +119 -0
- package/src/polyfills/sea.ts +33 -0
- package/src/polyfills/sqlite.ts +78 -0
- package/src/polyfills/stream.ts +1620 -0
- package/src/polyfills/string_decoder.ts +25 -0
- package/src/polyfills/tailwindcss-oxide.ts +309 -0
- package/src/polyfills/test.ts +197 -0
- package/src/polyfills/timers.ts +32 -0
- package/src/polyfills/tls.ts +105 -0
- package/src/polyfills/trace_events.ts +50 -0
- package/src/polyfills/tty.ts +71 -0
- package/src/polyfills/url.ts +174 -0
- package/src/polyfills/util.ts +559 -0
- package/src/polyfills/v8.ts +126 -0
- package/src/polyfills/vm.ts +132 -0
- package/src/polyfills/volume-registry.ts +15 -0
- package/src/polyfills/wasi.ts +44 -0
- package/src/polyfills/worker_threads.ts +326 -0
- package/src/polyfills/ws.ts +595 -0
- package/src/polyfills/zlib.ts +881 -0
- package/src/request-proxy.ts +716 -0
- package/src/script-engine.ts +3375 -0
- package/src/sdk/nodepod-fs.ts +93 -0
- package/src/sdk/nodepod-process.ts +86 -0
- package/src/sdk/nodepod-terminal.ts +350 -0
- package/src/sdk/nodepod.ts +509 -0
- package/src/sdk/types.ts +70 -0
- package/src/shell/commands/bun.ts +121 -0
- package/src/shell/commands/directory.ts +297 -0
- package/src/shell/commands/file-ops.ts +525 -0
- package/src/shell/commands/git.ts +2142 -0
- package/src/shell/commands/node.ts +80 -0
- package/src/shell/commands/npm.ts +198 -0
- package/src/shell/commands/pm-types.ts +45 -0
- package/src/shell/commands/pnpm.ts +82 -0
- package/src/shell/commands/search.ts +264 -0
- package/src/shell/commands/shell-env.ts +352 -0
- package/src/shell/commands/text-processing.ts +1152 -0
- package/src/shell/commands/yarn.ts +84 -0
- package/src/shell/shell-builtins.ts +19 -0
- package/src/shell/shell-helpers.ts +250 -0
- package/src/shell/shell-interpreter.ts +514 -0
- package/src/shell/shell-parser.ts +429 -0
- package/src/shell/shell-types.ts +85 -0
- package/src/syntax-transforms.ts +561 -0
- package/src/threading/engine-worker.ts +64 -0
- package/src/threading/inline-worker.ts +372 -0
- package/src/threading/offload-types.ts +112 -0
- package/src/threading/offload-worker.ts +383 -0
- package/src/threading/offload.ts +271 -0
- package/src/threading/process-context.ts +92 -0
- package/src/threading/process-handle.ts +275 -0
- package/src/threading/process-manager.ts +956 -0
- package/src/threading/process-worker-entry.ts +854 -0
- package/src/threading/shared-vfs.ts +352 -0
- package/src/threading/sync-channel.ts +135 -0
- package/src/threading/task-queue.ts +177 -0
- package/src/threading/vfs-bridge.ts +231 -0
- package/src/threading/worker-pool.ts +233 -0
- package/src/threading/worker-protocol.ts +358 -0
- package/src/threading/worker-vfs.ts +218 -0
- package/src/types/externals.d.ts +38 -0
- package/src/types/fs-streams.ts +142 -0
- package/src/types/manifest.ts +17 -0
- package/src/worker-sandbox.ts +90 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// ESM-to-CJS bulk transformer (esbuild-wasm). Used at install time to
|
|
2
|
+
// pre-convert packages so synchronous require() works at runtime.
|
|
3
|
+
// Transforms are offloaded to Web Workers when available.
|
|
4
|
+
|
|
5
|
+
import type { MemoryVolume } from "./memory-volume";
|
|
6
|
+
import { CDN_ESBUILD_ESM, CDN_ESBUILD_BINARY, cdnImport } from "./constants/cdn-urls";
|
|
7
|
+
import { offload, taskId, TaskPriority } from "./threading/offload";
|
|
8
|
+
import type { TransformResult } from "./threading/offload-types";
|
|
9
|
+
|
|
10
|
+
const inBrowser = typeof window !== "undefined";
|
|
11
|
+
|
|
12
|
+
// load and init esbuild-wasm from CDN (reuses existing instance)
|
|
13
|
+
export async function prepareTransformer(): Promise<void> {
|
|
14
|
+
if (!inBrowser) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (window.__esbuildEngine) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (window.__esbuildReady) {
|
|
23
|
+
return window.__esbuildReady;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
window.__esbuildReady = (async () => {
|
|
27
|
+
try {
|
|
28
|
+
const loaded = await cdnImport(CDN_ESBUILD_ESM);
|
|
29
|
+
const engine = loaded.default || loaded;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await engine.initialize({ wasmURL: CDN_ESBUILD_BINARY });
|
|
33
|
+
} catch (initErr) {
|
|
34
|
+
if (
|
|
35
|
+
initErr instanceof Error &&
|
|
36
|
+
initErr.message.includes('Cannot call "initialize" more than once')
|
|
37
|
+
) {
|
|
38
|
+
/* already initialized, ignore */
|
|
39
|
+
} else {
|
|
40
|
+
throw initErr;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
window.__esbuildEngine = engine;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
window.__esbuildReady = undefined;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
return window.__esbuildReady;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isTransformerLoaded(): boolean {
|
|
55
|
+
if (!inBrowser) return true;
|
|
56
|
+
return window.__esbuildEngine !== undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function containsJsx(source: string): boolean {
|
|
60
|
+
if (/<[A-Z][a-zA-Z0-9.]*[\s/>]/.test(source)) return true;
|
|
61
|
+
if (/<\/[a-zA-Z]/.test(source)) return true;
|
|
62
|
+
if (/\/>/.test(source)) return true;
|
|
63
|
+
if (/<>|<\/>/.test(source)) return true;
|
|
64
|
+
if (/React\.createElement\b/.test(source)) return true;
|
|
65
|
+
if (/jsx\(|jsxs\(|jsxDEV\(/.test(source)) return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function detectLoader(
|
|
70
|
+
filePath: string,
|
|
71
|
+
source: string,
|
|
72
|
+
): "js" | "jsx" | "ts" | "tsx" {
|
|
73
|
+
if (filePath.endsWith(".jsx")) return "jsx";
|
|
74
|
+
if (filePath.endsWith(".ts")) return "ts";
|
|
75
|
+
if (filePath.endsWith(".tsx")) return "tsx";
|
|
76
|
+
if (filePath.endsWith(".mjs")) return "js";
|
|
77
|
+
if (containsJsx(source)) return "jsx";
|
|
78
|
+
return "js";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// convert a single file ESM->CJS via offloaded esbuild.transform()
|
|
82
|
+
export async function convertFile(
|
|
83
|
+
source: string,
|
|
84
|
+
filePath: string,
|
|
85
|
+
): Promise<string> {
|
|
86
|
+
if (!inBrowser) return source;
|
|
87
|
+
|
|
88
|
+
const result: TransformResult = await offload({
|
|
89
|
+
type: "transform",
|
|
90
|
+
id: taskId(),
|
|
91
|
+
source,
|
|
92
|
+
filePath,
|
|
93
|
+
options: {
|
|
94
|
+
loader: detectLoader(filePath, source),
|
|
95
|
+
format: "cjs",
|
|
96
|
+
target: "esnext",
|
|
97
|
+
platform: "neutral",
|
|
98
|
+
define: {
|
|
99
|
+
"import.meta.url": "import_meta.url",
|
|
100
|
+
"import.meta.dirname": "import_meta.dirname",
|
|
101
|
+
"import.meta.filename": "import_meta.filename",
|
|
102
|
+
"import.meta": "import_meta",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
priority: TaskPriority.NORMAL,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// patch dynamic imports of node builtins to synchronous require()
|
|
109
|
+
let code = result.code;
|
|
110
|
+
code = code.replace(
|
|
111
|
+
/\bimport\s*\(\s*["']node:([^"']+)["']\s*\)/g,
|
|
112
|
+
'Promise.resolve(require("node:$1"))',
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const coreModules = [
|
|
116
|
+
"assert", "buffer", "child_process", "cluster", "crypto",
|
|
117
|
+
"dgram", "dns", "events", "fs", "http", "http2", "https",
|
|
118
|
+
"net", "os", "path", "perf_hooks", "querystring", "readline",
|
|
119
|
+
"stream", "string_decoder", "timers", "tls", "url", "util",
|
|
120
|
+
"v8", "vm", "worker_threads", "zlib", "async_hooks",
|
|
121
|
+
"inspector", "module",
|
|
122
|
+
];
|
|
123
|
+
for (const mod of coreModules) {
|
|
124
|
+
const re = new RegExp(`\\bimport\\s*\\(\\s*["']${mod}["']\\s*\\)`, "g");
|
|
125
|
+
code = code.replace(re, `Promise.resolve(require("${mod}"))`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return code;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// direct main-thread esbuild transform (no worker offloading).
|
|
132
|
+
// used as the fallback path to avoid circular offload -> convertFile -> offload.
|
|
133
|
+
export async function convertFileDirect(
|
|
134
|
+
source: string,
|
|
135
|
+
filePath: string,
|
|
136
|
+
): Promise<string> {
|
|
137
|
+
if (!inBrowser) return source;
|
|
138
|
+
|
|
139
|
+
if (!window.__esbuildEngine) await prepareTransformer();
|
|
140
|
+
const engine = window.__esbuildEngine;
|
|
141
|
+
if (!engine) throw new Error("esbuild engine not available");
|
|
142
|
+
|
|
143
|
+
const loader = detectLoader(filePath, source);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const output = await engine.transform(source, {
|
|
147
|
+
loader,
|
|
148
|
+
format: "cjs",
|
|
149
|
+
target: "esnext",
|
|
150
|
+
platform: "neutral",
|
|
151
|
+
define: {
|
|
152
|
+
"import.meta.url": "import_meta.url",
|
|
153
|
+
"import.meta.dirname": "import_meta.dirname",
|
|
154
|
+
"import.meta.filename": "import_meta.filename",
|
|
155
|
+
"import.meta": "import_meta",
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return output.code;
|
|
159
|
+
} catch (err: unknown) {
|
|
160
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
161
|
+
|
|
162
|
+
// retry with alternative loaders
|
|
163
|
+
if (loader === "js" || loader === "jsx") {
|
|
164
|
+
const fallbacks = loader === "js" ? ["jsx", "tsx", "ts"] : ["tsx"];
|
|
165
|
+
for (const fallbackLoader of fallbacks) {
|
|
166
|
+
try {
|
|
167
|
+
const output = await engine.transform(source, {
|
|
168
|
+
loader: fallbackLoader as "jsx" | "tsx" | "ts",
|
|
169
|
+
format: "cjs",
|
|
170
|
+
target: "esnext",
|
|
171
|
+
platform: "neutral",
|
|
172
|
+
define: {
|
|
173
|
+
"import.meta.url": "import_meta.url",
|
|
174
|
+
"import.meta.dirname": "import_meta.dirname",
|
|
175
|
+
"import.meta.filename": "import_meta.filename",
|
|
176
|
+
"import.meta": "import_meta",
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
return output.code;
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (msg.includes("Top-level await")) {
|
|
186
|
+
try {
|
|
187
|
+
const output = await engine.transform(source, {
|
|
188
|
+
loader,
|
|
189
|
+
format: "esm",
|
|
190
|
+
target: "esnext",
|
|
191
|
+
platform: "neutral",
|
|
192
|
+
define: {
|
|
193
|
+
"import.meta.url": "import_meta.url",
|
|
194
|
+
"import.meta.dirname": "import_meta.dirname",
|
|
195
|
+
"import.meta.filename": "import_meta.filename",
|
|
196
|
+
"import.meta": "import_meta",
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
return output.code;
|
|
200
|
+
} catch {
|
|
201
|
+
return source;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return source;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function requiresConversion(filePath: string, source: string): boolean {
|
|
209
|
+
// .mjs must stay ESM in the VFS -- tools read them directly. the runtime
|
|
210
|
+
// handles ESM->CJS on-the-fly for require().
|
|
211
|
+
if (filePath.endsWith(".mjs")) return false;
|
|
212
|
+
if (filePath.endsWith(".cjs")) return false;
|
|
213
|
+
return (
|
|
214
|
+
/\bimport\s+[\w{*'"]/m.test(source) ||
|
|
215
|
+
/\bexport\s+(?:default|const|let|var|function|class|{|\*)/m.test(source) ||
|
|
216
|
+
/\bimport\.meta\b/.test(source)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function needsBuiltinPatching(source: string): boolean {
|
|
221
|
+
return (
|
|
222
|
+
/\bimport\s*\(\s*["']node:/.test(source) ||
|
|
223
|
+
/\bimport\s*\(\s*["'](fs|path|http|https|net|url|util|events|stream|os|crypto)["']/.test(
|
|
224
|
+
source,
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function patchBuiltinImports(source: string): string {
|
|
230
|
+
let patched = source;
|
|
231
|
+
patched = patched.replace(
|
|
232
|
+
/\bimport\s*\(\s*["']node:([^"']+)["']\s*\)/g,
|
|
233
|
+
'Promise.resolve(require("node:$1"))',
|
|
234
|
+
);
|
|
235
|
+
const builtins = [
|
|
236
|
+
"assert",
|
|
237
|
+
"buffer",
|
|
238
|
+
"child_process",
|
|
239
|
+
"cluster",
|
|
240
|
+
"crypto",
|
|
241
|
+
"dgram",
|
|
242
|
+
"dns",
|
|
243
|
+
"events",
|
|
244
|
+
"fs",
|
|
245
|
+
"http",
|
|
246
|
+
"http2",
|
|
247
|
+
"https",
|
|
248
|
+
"net",
|
|
249
|
+
"os",
|
|
250
|
+
"path",
|
|
251
|
+
"perf_hooks",
|
|
252
|
+
"querystring",
|
|
253
|
+
"readline",
|
|
254
|
+
"stream",
|
|
255
|
+
"string_decoder",
|
|
256
|
+
"timers",
|
|
257
|
+
"tls",
|
|
258
|
+
"url",
|
|
259
|
+
"util",
|
|
260
|
+
"v8",
|
|
261
|
+
"vm",
|
|
262
|
+
"worker_threads",
|
|
263
|
+
"zlib",
|
|
264
|
+
"async_hooks",
|
|
265
|
+
"inspector",
|
|
266
|
+
"module",
|
|
267
|
+
];
|
|
268
|
+
for (const b of builtins) {
|
|
269
|
+
patched = patched.replace(
|
|
270
|
+
new RegExp(`\\bimport\\s*\\(\\s*["']${b}["']\\s*\\)`, "g"),
|
|
271
|
+
`Promise.resolve(require("${b}"))`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return patched;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function listJsFiles(vol: MemoryVolume, dir: string): string[] {
|
|
278
|
+
const result: string[] = [];
|
|
279
|
+
try {
|
|
280
|
+
for (const name of vol.readdirSync(dir)) {
|
|
281
|
+
const full = dir + "/" + name;
|
|
282
|
+
try {
|
|
283
|
+
const info = vol.statSync(full);
|
|
284
|
+
if (info.isDirectory()) {
|
|
285
|
+
if (name !== "node_modules") result.push(...listJsFiles(vol, full));
|
|
286
|
+
} else if (
|
|
287
|
+
name.endsWith(".js") ||
|
|
288
|
+
name.endsWith(".mjs") ||
|
|
289
|
+
name.endsWith(".jsx")
|
|
290
|
+
) {
|
|
291
|
+
result.push(full);
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isEsmPackage(vol: MemoryVolume, packageDir: string): boolean {
|
|
302
|
+
try {
|
|
303
|
+
const pkgPath = packageDir + "/package.json";
|
|
304
|
+
if (vol.existsSync(pkgPath)) {
|
|
305
|
+
const raw = vol.readFileSync(pkgPath, "utf8");
|
|
306
|
+
const pkg = JSON.parse(raw);
|
|
307
|
+
return pkg.type === "module";
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// convert all ESM files in a package directory to CJS
|
|
315
|
+
export async function convertPackage(
|
|
316
|
+
vol: MemoryVolume,
|
|
317
|
+
packageDir: string,
|
|
318
|
+
onProgress?: (msg: string) => void,
|
|
319
|
+
): Promise<number> {
|
|
320
|
+
let converted = 0;
|
|
321
|
+
const files = listJsFiles(vol, packageDir);
|
|
322
|
+
onProgress?.(` Converting ${files.length} files in ${packageDir}...`);
|
|
323
|
+
|
|
324
|
+
// "type":"module" packages stay as ESM in VFS -- tools like Vite read them
|
|
325
|
+
// directly and expect ESM syntax. require() handles conversion at runtime.
|
|
326
|
+
const esmPackage = isEsmPackage(vol, packageDir);
|
|
327
|
+
|
|
328
|
+
const BATCH = 50;
|
|
329
|
+
for (let i = 0; i < files.length; i += BATCH) {
|
|
330
|
+
const batch = files.slice(i, i + BATCH);
|
|
331
|
+
await Promise.all(
|
|
332
|
+
batch.map(async (fp) => {
|
|
333
|
+
try {
|
|
334
|
+
const src = vol.readFileSync(fp, "utf8");
|
|
335
|
+
|
|
336
|
+
if (esmPackage) {
|
|
337
|
+
if (needsBuiltinPatching(src)) {
|
|
338
|
+
vol.writeFileSync(fp, patchBuiltinImports(src));
|
|
339
|
+
converted++;
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// .mjs stays ESM even in non-ESM packages
|
|
345
|
+
if (fp.endsWith(".mjs")) {
|
|
346
|
+
if (needsBuiltinPatching(src)) {
|
|
347
|
+
vol.writeFileSync(fp, patchBuiltinImports(src));
|
|
348
|
+
converted++;
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (requiresConversion(fp, src)) {
|
|
354
|
+
const out = await convertFile(src, fp);
|
|
355
|
+
vol.writeFileSync(fp, out);
|
|
356
|
+
converted++;
|
|
357
|
+
} else if (needsBuiltinPatching(src)) {
|
|
358
|
+
vol.writeFileSync(fp, patchBuiltinImports(src));
|
|
359
|
+
converted++;
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return converted;
|
|
368
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Archive Extractor — downloads .tgz from npm, decompresses, parses tar, writes to VFS.
|
|
2
|
+
// Heavy work is offloaded to web workers when available.
|
|
3
|
+
|
|
4
|
+
import pako from "pako";
|
|
5
|
+
import { MemoryVolume } from "../memory-volume";
|
|
6
|
+
import * as path from "../polyfills/path";
|
|
7
|
+
import { offload, taskId, TaskPriority } from "../threading/offload";
|
|
8
|
+
import type { ExtractResult } from "../threading/offload-types";
|
|
9
|
+
import { base64ToBytes } from "../helpers/byte-encoding";
|
|
10
|
+
import { precompileWasm } from "../helpers/wasm-cache";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Public types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface ExtractionOptions {
|
|
17
|
+
// default 1 — strips npm's "package/" prefix
|
|
18
|
+
stripComponents?: number;
|
|
19
|
+
filter?: (entryPath: string) => boolean;
|
|
20
|
+
onProgress?: (msg: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Internal tar structures
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
type EntryKind = "file" | "directory" | "link" | "other";
|
|
28
|
+
|
|
29
|
+
interface ArchiveEntry {
|
|
30
|
+
filepath: string;
|
|
31
|
+
kind: EntryKind;
|
|
32
|
+
byteSize: number;
|
|
33
|
+
fileMode: number;
|
|
34
|
+
payload?: Uint8Array;
|
|
35
|
+
linkDestination?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Tar header helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function readNullTerminated(
|
|
43
|
+
buf: Uint8Array,
|
|
44
|
+
start: number,
|
|
45
|
+
len: number,
|
|
46
|
+
): string {
|
|
47
|
+
const slice = buf.slice(start, start + len);
|
|
48
|
+
const zeroPos = slice.indexOf(0);
|
|
49
|
+
const trimmed = zeroPos >= 0 ? slice.slice(0, zeroPos) : slice;
|
|
50
|
+
return new TextDecoder().decode(trimmed);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readOctalField(buf: Uint8Array, start: number, len: number): number {
|
|
54
|
+
const raw = readNullTerminated(buf, start, len).trim();
|
|
55
|
+
return parseInt(raw, 8) || 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function classifyTypeFlag(flag: string): EntryKind {
|
|
59
|
+
switch (flag) {
|
|
60
|
+
case "0":
|
|
61
|
+
case "\0":
|
|
62
|
+
case "":
|
|
63
|
+
return "file";
|
|
64
|
+
case "5":
|
|
65
|
+
return "directory";
|
|
66
|
+
case "1":
|
|
67
|
+
case "2":
|
|
68
|
+
return "link";
|
|
69
|
+
default:
|
|
70
|
+
return "other";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Tar parser (generator)
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export function* parseTarArchive(raw: Uint8Array): Generator<ArchiveEntry> {
|
|
79
|
+
const BLOCK = 512;
|
|
80
|
+
let cursor = 0;
|
|
81
|
+
|
|
82
|
+
while (cursor + BLOCK <= raw.length) {
|
|
83
|
+
const headerBlock = raw.slice(cursor, cursor + BLOCK);
|
|
84
|
+
cursor += BLOCK;
|
|
85
|
+
|
|
86
|
+
// two zero blocks = end of archive
|
|
87
|
+
const allZero = headerBlock.every((b) => b === 0);
|
|
88
|
+
if (allZero) break;
|
|
89
|
+
|
|
90
|
+
const nameField = readNullTerminated(headerBlock, 0, 100);
|
|
91
|
+
if (!nameField) continue;
|
|
92
|
+
|
|
93
|
+
const fileMode = readOctalField(headerBlock, 100, 8);
|
|
94
|
+
const byteSize = readOctalField(headerBlock, 124, 12);
|
|
95
|
+
const typeChar = String.fromCharCode(headerBlock[156]);
|
|
96
|
+
const linkField = readNullTerminated(headerBlock, 157, 100);
|
|
97
|
+
const prefixField = readNullTerminated(headerBlock, 345, 155);
|
|
98
|
+
|
|
99
|
+
const filepath = prefixField ? `${prefixField}/${nameField}` : nameField;
|
|
100
|
+
const kind = classifyTypeFlag(typeChar);
|
|
101
|
+
|
|
102
|
+
let payload: Uint8Array | undefined;
|
|
103
|
+
if (kind === "file") {
|
|
104
|
+
payload =
|
|
105
|
+
byteSize > 0 ? raw.slice(cursor, cursor + byteSize) : new Uint8Array(0);
|
|
106
|
+
if (byteSize > 0) {
|
|
107
|
+
cursor += Math.ceil(byteSize / BLOCK) * BLOCK;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
yield {
|
|
112
|
+
filepath,
|
|
113
|
+
kind,
|
|
114
|
+
byteSize,
|
|
115
|
+
fileMode,
|
|
116
|
+
payload,
|
|
117
|
+
linkDestination: kind === "link" ? linkField : undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Decompression
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
export function inflateGzip(compressed: ArrayBuffer | Uint8Array): Uint8Array {
|
|
127
|
+
const input =
|
|
128
|
+
compressed instanceof Uint8Array ? compressed : new Uint8Array(compressed);
|
|
129
|
+
return pako.inflate(input);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Extraction into MemoryVolume
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
export function extractArchive(
|
|
137
|
+
archiveBytes: ArrayBuffer | Uint8Array,
|
|
138
|
+
vol: MemoryVolume,
|
|
139
|
+
destDir: string,
|
|
140
|
+
opts: ExtractionOptions = {},
|
|
141
|
+
): string[] {
|
|
142
|
+
const { stripComponents = 1, filter, onProgress } = opts;
|
|
143
|
+
|
|
144
|
+
onProgress?.("Inflating archive...");
|
|
145
|
+
const tarBytes = inflateGzip(archiveBytes);
|
|
146
|
+
|
|
147
|
+
const writtenPaths: string[] = [];
|
|
148
|
+
|
|
149
|
+
for (const entry of parseTarArchive(tarBytes)) {
|
|
150
|
+
if (entry.kind !== "file" && entry.kind !== "directory") continue;
|
|
151
|
+
|
|
152
|
+
let relative = entry.filepath;
|
|
153
|
+
if (stripComponents > 0) {
|
|
154
|
+
const segments = relative.split("/").filter(Boolean);
|
|
155
|
+
if (segments.length <= stripComponents) continue;
|
|
156
|
+
relative = segments.slice(stripComponents).join("/");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (filter && !filter(relative)) continue;
|
|
160
|
+
|
|
161
|
+
const absolute = path.join(destDir, relative);
|
|
162
|
+
|
|
163
|
+
if (entry.kind === "directory") {
|
|
164
|
+
vol.mkdirSync(absolute, { recursive: true });
|
|
165
|
+
} else if (entry.kind === "file" && entry.payload) {
|
|
166
|
+
const parentDir = path.dirname(absolute);
|
|
167
|
+
vol.mkdirSync(parentDir, { recursive: true });
|
|
168
|
+
vol.writeFileSync(absolute, entry.payload);
|
|
169
|
+
// pre-compile so it's ready by the time code needs it
|
|
170
|
+
if (absolute.endsWith(".wasm")) {
|
|
171
|
+
precompileWasm(entry.payload);
|
|
172
|
+
}
|
|
173
|
+
writtenPaths.push(absolute);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
onProgress?.(`Extracted ${writtenPaths.length} files`);
|
|
178
|
+
return writtenPaths;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// High-level: download + extract in one step
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
// Offloads fetch + decompress + parse to a worker, then writes results to VFS on main thread
|
|
186
|
+
export async function downloadAndExtract(
|
|
187
|
+
url: string,
|
|
188
|
+
vol: MemoryVolume,
|
|
189
|
+
destDir: string,
|
|
190
|
+
opts: ExtractionOptions = {},
|
|
191
|
+
): Promise<string[]> {
|
|
192
|
+
opts.onProgress?.(`Fetching ${url}...`);
|
|
193
|
+
|
|
194
|
+
const result: ExtractResult = await offload({
|
|
195
|
+
type: "extract",
|
|
196
|
+
id: taskId(),
|
|
197
|
+
tarballUrl: url,
|
|
198
|
+
stripComponents: opts.stripComponents ?? 1,
|
|
199
|
+
priority: TaskPriority.NORMAL,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const writtenPaths: string[] = [];
|
|
203
|
+
for (const file of result.files) {
|
|
204
|
+
if (opts.filter && !opts.filter(file.path)) continue;
|
|
205
|
+
|
|
206
|
+
const absolute = path.join(destDir, file.path);
|
|
207
|
+
const parentDir = path.dirname(absolute);
|
|
208
|
+
vol.mkdirSync(parentDir, { recursive: true });
|
|
209
|
+
|
|
210
|
+
if (file.isBinary) {
|
|
211
|
+
vol.writeFileSync(absolute, base64ToBytes(file.data));
|
|
212
|
+
} else {
|
|
213
|
+
vol.writeFileSync(absolute, file.data);
|
|
214
|
+
}
|
|
215
|
+
writtenPaths.push(absolute);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
opts.onProgress?.(`Extracted ${writtenPaths.length} files`);
|
|
219
|
+
return writtenPaths;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Main-thread fallback when workers aren't available
|
|
223
|
+
export async function downloadAndExtractDirect(
|
|
224
|
+
url: string,
|
|
225
|
+
vol: MemoryVolume,
|
|
226
|
+
destDir: string,
|
|
227
|
+
opts: ExtractionOptions = {},
|
|
228
|
+
): Promise<string[]> {
|
|
229
|
+
opts.onProgress?.(`Fetching ${url}...`);
|
|
230
|
+
|
|
231
|
+
const response = await fetch(url);
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Archive download failed (HTTP ${response.status}): ${url}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const rawBytes = await response.arrayBuffer();
|
|
239
|
+
return extractArchive(rawBytes, vol, destDir, opts);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default {
|
|
243
|
+
downloadAndExtract,
|
|
244
|
+
downloadAndExtractDirect,
|
|
245
|
+
parseTarArchive,
|
|
246
|
+
extractArchive,
|
|
247
|
+
inflateGzip,
|
|
248
|
+
};
|