@scelar/nodepod 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/LICENSE +43 -0
  2. package/README.md +240 -0
  3. package/dist/child_process-BJOMsZje.js +8233 -0
  4. package/dist/child_process-BJOMsZje.js.map +1 -0
  5. package/dist/child_process-Cj8vOcuc.cjs +7434 -0
  6. package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
  7. package/dist/index-Cb1Cgdnd.js +35308 -0
  8. package/dist/index-Cb1Cgdnd.js.map +1 -0
  9. package/dist/index-DsMGS-xc.cjs +37195 -0
  10. package/dist/index-DsMGS-xc.cjs.map +1 -0
  11. package/dist/index.cjs +65 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +59 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +95 -0
  16. package/src/__tests__/smoke.test.ts +11 -0
  17. package/src/constants/cdn-urls.ts +18 -0
  18. package/src/constants/config.ts +236 -0
  19. package/src/cross-origin.ts +26 -0
  20. package/src/engine-factory.ts +176 -0
  21. package/src/engine-types.ts +56 -0
  22. package/src/helpers/byte-encoding.ts +39 -0
  23. package/src/helpers/digest.ts +9 -0
  24. package/src/helpers/event-loop.ts +96 -0
  25. package/src/helpers/wasm-cache.ts +133 -0
  26. package/src/iframe-sandbox.ts +141 -0
  27. package/src/index.ts +192 -0
  28. package/src/isolation-helpers.ts +148 -0
  29. package/src/memory-volume.ts +941 -0
  30. package/src/module-transformer.ts +368 -0
  31. package/src/packages/archive-extractor.ts +248 -0
  32. package/src/packages/browser-bundler.ts +284 -0
  33. package/src/packages/installer.ts +396 -0
  34. package/src/packages/registry-client.ts +131 -0
  35. package/src/packages/version-resolver.ts +411 -0
  36. package/src/polyfills/assert.ts +384 -0
  37. package/src/polyfills/async_hooks.ts +144 -0
  38. package/src/polyfills/buffer.ts +628 -0
  39. package/src/polyfills/child_process.ts +2288 -0
  40. package/src/polyfills/chokidar.ts +336 -0
  41. package/src/polyfills/cluster.ts +106 -0
  42. package/src/polyfills/console.ts +136 -0
  43. package/src/polyfills/constants.ts +123 -0
  44. package/src/polyfills/crypto.ts +885 -0
  45. package/src/polyfills/dgram.ts +87 -0
  46. package/src/polyfills/diagnostics_channel.ts +76 -0
  47. package/src/polyfills/dns.ts +134 -0
  48. package/src/polyfills/domain.ts +68 -0
  49. package/src/polyfills/esbuild.ts +854 -0
  50. package/src/polyfills/events.ts +276 -0
  51. package/src/polyfills/fs.ts +2888 -0
  52. package/src/polyfills/fsevents.ts +79 -0
  53. package/src/polyfills/http.ts +1449 -0
  54. package/src/polyfills/http2.ts +199 -0
  55. package/src/polyfills/https.ts +76 -0
  56. package/src/polyfills/inspector.ts +62 -0
  57. package/src/polyfills/lightningcss.ts +105 -0
  58. package/src/polyfills/module.ts +191 -0
  59. package/src/polyfills/net.ts +353 -0
  60. package/src/polyfills/os.ts +238 -0
  61. package/src/polyfills/path.ts +206 -0
  62. package/src/polyfills/perf_hooks.ts +102 -0
  63. package/src/polyfills/process.ts +690 -0
  64. package/src/polyfills/punycode.ts +159 -0
  65. package/src/polyfills/querystring.ts +93 -0
  66. package/src/polyfills/quic.ts +118 -0
  67. package/src/polyfills/readdirp.ts +229 -0
  68. package/src/polyfills/readline.ts +692 -0
  69. package/src/polyfills/repl.ts +134 -0
  70. package/src/polyfills/rollup.ts +119 -0
  71. package/src/polyfills/sea.ts +33 -0
  72. package/src/polyfills/sqlite.ts +78 -0
  73. package/src/polyfills/stream.ts +1620 -0
  74. package/src/polyfills/string_decoder.ts +25 -0
  75. package/src/polyfills/tailwindcss-oxide.ts +309 -0
  76. package/src/polyfills/test.ts +197 -0
  77. package/src/polyfills/timers.ts +32 -0
  78. package/src/polyfills/tls.ts +105 -0
  79. package/src/polyfills/trace_events.ts +50 -0
  80. package/src/polyfills/tty.ts +71 -0
  81. package/src/polyfills/url.ts +174 -0
  82. package/src/polyfills/util.ts +559 -0
  83. package/src/polyfills/v8.ts +126 -0
  84. package/src/polyfills/vm.ts +132 -0
  85. package/src/polyfills/volume-registry.ts +15 -0
  86. package/src/polyfills/wasi.ts +44 -0
  87. package/src/polyfills/worker_threads.ts +326 -0
  88. package/src/polyfills/ws.ts +595 -0
  89. package/src/polyfills/zlib.ts +881 -0
  90. package/src/request-proxy.ts +716 -0
  91. package/src/script-engine.ts +3375 -0
  92. package/src/sdk/nodepod-fs.ts +93 -0
  93. package/src/sdk/nodepod-process.ts +86 -0
  94. package/src/sdk/nodepod-terminal.ts +350 -0
  95. package/src/sdk/nodepod.ts +509 -0
  96. package/src/sdk/types.ts +70 -0
  97. package/src/shell/commands/bun.ts +121 -0
  98. package/src/shell/commands/directory.ts +297 -0
  99. package/src/shell/commands/file-ops.ts +525 -0
  100. package/src/shell/commands/git.ts +2142 -0
  101. package/src/shell/commands/node.ts +80 -0
  102. package/src/shell/commands/npm.ts +198 -0
  103. package/src/shell/commands/pm-types.ts +45 -0
  104. package/src/shell/commands/pnpm.ts +82 -0
  105. package/src/shell/commands/search.ts +264 -0
  106. package/src/shell/commands/shell-env.ts +352 -0
  107. package/src/shell/commands/text-processing.ts +1152 -0
  108. package/src/shell/commands/yarn.ts +84 -0
  109. package/src/shell/shell-builtins.ts +19 -0
  110. package/src/shell/shell-helpers.ts +250 -0
  111. package/src/shell/shell-interpreter.ts +514 -0
  112. package/src/shell/shell-parser.ts +429 -0
  113. package/src/shell/shell-types.ts +85 -0
  114. package/src/syntax-transforms.ts +561 -0
  115. package/src/threading/engine-worker.ts +64 -0
  116. package/src/threading/inline-worker.ts +372 -0
  117. package/src/threading/offload-types.ts +112 -0
  118. package/src/threading/offload-worker.ts +383 -0
  119. package/src/threading/offload.ts +271 -0
  120. package/src/threading/process-context.ts +92 -0
  121. package/src/threading/process-handle.ts +275 -0
  122. package/src/threading/process-manager.ts +956 -0
  123. package/src/threading/process-worker-entry.ts +854 -0
  124. package/src/threading/shared-vfs.ts +352 -0
  125. package/src/threading/sync-channel.ts +135 -0
  126. package/src/threading/task-queue.ts +177 -0
  127. package/src/threading/vfs-bridge.ts +231 -0
  128. package/src/threading/worker-pool.ts +233 -0
  129. package/src/threading/worker-protocol.ts +358 -0
  130. package/src/threading/worker-vfs.ts +218 -0
  131. package/src/types/externals.d.ts +38 -0
  132. package/src/types/fs-streams.ts +142 -0
  133. package/src/types/manifest.ts +17 -0
  134. package/src/worker-sandbox.ts +90 -0
@@ -0,0 +1,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
+ };