@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,284 @@
1
+ // Browser Bundler — bundles node_modules packages into single ESM files for the browser.
2
+ // Uses esbuild-wasm. React-family packages are kept external (shared via import map).
3
+
4
+ import {
5
+ build,
6
+ setVolume as setVFS,
7
+ type BundleOutput as BuildResult,
8
+ } from "../polyfills/esbuild";
9
+ import type { MemoryVolume } from "../memory-volume";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Module-level state
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const bundledModules = new Map<string, string>();
16
+ let activeVolume: MemoryVolume | null = null;
17
+ let externalPackages: string[] = [];
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Public API
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export function invalidateBundleCache(): void {
24
+ bundledModules.clear();
25
+ }
26
+
27
+ // Must be called before bundleForBrowser
28
+ export function attachVolume(vol: MemoryVolume): void {
29
+ setVFS(vol);
30
+ activeVolume = vol;
31
+ }
32
+
33
+ export function setExternalPackages(packages: string[]): void {
34
+ externalPackages = [...packages];
35
+ }
36
+
37
+ // Bundle a bare specifier (e.g. "zod", "lodash/merge") into a self-contained ESM string
38
+ export async function bundleForBrowser(specifier: string): Promise<string> {
39
+ const hit = bundledModules.get(specifier);
40
+ if (hit) return hit;
41
+
42
+ const entryFile = locateEntryPoint(specifier);
43
+
44
+ let knownExports: string[] = [];
45
+ if (entryFile && activeVolume) {
46
+ try {
47
+ const src = activeVolume.readFileSync(entryFile, "utf8");
48
+ knownExports = detectCjsExports(src);
49
+ } catch {
50
+ // non-critical
51
+ }
52
+ }
53
+
54
+ let outcome: BuildResult;
55
+
56
+ if (entryFile) {
57
+ outcome = await build({
58
+ entryPoints: [entryFile],
59
+ bundle: true,
60
+ format: "esm",
61
+ target: "esnext",
62
+ external: externalPackages,
63
+ write: false,
64
+ });
65
+ } else {
66
+ // can't determine entry point — let esbuild resolve it via a re-export stub
67
+ const stub = `export * from '${specifier}';\n`;
68
+ outcome = await build({
69
+ stdin: {
70
+ contents: stub,
71
+ resolveDir: "/node_modules",
72
+ loader: "js",
73
+ },
74
+ bundle: true,
75
+ format: "esm",
76
+ target: "esnext",
77
+ external: externalPackages,
78
+ write: false,
79
+ });
80
+ }
81
+
82
+ if (!outcome.outputFiles || outcome.outputFiles.length === 0) {
83
+ throw new Error(`Bundling produced no output for "${specifier}"`);
84
+ }
85
+
86
+ const outputRecord = outcome.outputFiles[0];
87
+ let esmCode = outputRecord.text;
88
+ if (!esmCode && outputRecord.contents?.length > 0) {
89
+ esmCode = new TextDecoder().decode(outputRecord.contents);
90
+ }
91
+
92
+ if (!esmCode) {
93
+ throw new Error(
94
+ `Bundling produced empty output for "${specifier}" (entry: ${entryFile || "stdin"})`,
95
+ );
96
+ }
97
+
98
+ esmCode = rewriteExternalRequireCalls(esmCode);
99
+ if (knownExports.length > 0) {
100
+ esmCode = injectNamedExports(esmCode, knownExports);
101
+ }
102
+
103
+ bundledModules.set(specifier, esmCode);
104
+ return esmCode;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Entry-point resolution
109
+ // ---------------------------------------------------------------------------
110
+
111
+ // CJS first to avoid .mjs confusion
112
+ const CONDITION_ORDER = ["require", "import", "module", "default"] as const;
113
+
114
+ // Walk nested export conditions until we hit an actual file path string
115
+ function drillCondition(node: unknown): string | undefined {
116
+ if (typeof node === "string") return node;
117
+ if (typeof node === "object" && node !== null) {
118
+ const obj = node as Record<string, unknown>;
119
+ for (const key of CONDITION_ORDER) {
120
+ if (key in obj) {
121
+ const found = drillCondition(obj[key]);
122
+ if (found) return found;
123
+ }
124
+ }
125
+ }
126
+ return undefined;
127
+ }
128
+
129
+ function fileExists(filePath: string): boolean {
130
+ if (!activeVolume) return false;
131
+ if (!activeVolume.existsSync(filePath)) return false;
132
+ try {
133
+ return !activeVolume.statSync(filePath).isDirectory();
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ // Resolve bare specifier to absolute path. Handles scoped packages, exports map, and fallbacks.
140
+ function locateEntryPoint(specifier: string): string | null {
141
+ if (!activeVolume) return null;
142
+
143
+ const segments = specifier.split("/");
144
+ const isScoped = segments[0].startsWith("@");
145
+ const pkgName = isScoped ? segments.slice(0, 2).join("/") : segments[0];
146
+ const subPath = isScoped
147
+ ? segments.slice(2).join("/")
148
+ : segments.slice(1).join("/");
149
+
150
+ const pkgRoot = `/node_modules/${pkgName}`;
151
+ const manifestFile = `${pkgRoot}/package.json`;
152
+ if (!activeVolume.existsSync(manifestFile)) return null;
153
+
154
+ try {
155
+ const manifest = JSON.parse(
156
+ activeVolume.readFileSync(manifestFile, "utf8"),
157
+ );
158
+
159
+ if (manifest.exports && typeof manifest.exports === "object") {
160
+ const conditionKey = subPath ? `./${subPath}` : ".";
161
+ const mapEntry = (manifest.exports as Record<string, unknown>)[
162
+ conditionKey
163
+ ];
164
+ if (mapEntry) {
165
+ const resolved = drillCondition(mapEntry);
166
+ if (resolved) {
167
+ const abs = `${pkgRoot}/${resolved.replace(/^\.\//, "")}`;
168
+ if (fileExists(abs)) return abs;
169
+ }
170
+ }
171
+ }
172
+
173
+ // fallback: main/module fields (root export only)
174
+ if (!subPath) {
175
+ const fallbackEntry = manifest.main || manifest.module;
176
+ if (fallbackEntry) {
177
+ const abs = `${pkgRoot}/${fallbackEntry.replace(/^\.\//, "")}`;
178
+ if (fileExists(abs)) return abs;
179
+ }
180
+ const defaultIndex = `${pkgRoot}/index.js`;
181
+ if (fileExists(defaultIndex)) return defaultIndex;
182
+ } else {
183
+ const basePath = `${pkgRoot}/${subPath}`;
184
+ for (const ext of ["", ".js", ".cjs", ".mjs", ".json"]) {
185
+ if (fileExists(basePath + ext)) return basePath + ext;
186
+ }
187
+ }
188
+ } catch {
189
+ // non-fatal
190
+ }
191
+
192
+ return null;
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // CJS export detection
197
+ // ---------------------------------------------------------------------------
198
+
199
+ // Detect named exports from CJS — looks for esbuild's __export() helper or exports.X = patterns
200
+ function detectCjsExports(source: string): string[] {
201
+ const helperMatch = source.match(/__export\(\w+,\s*\{([^}]+)\}/);
202
+ if (helperMatch) {
203
+ return [...helperMatch[1].matchAll(/(\w+)\s*:/g)]
204
+ .map((m) => m[1])
205
+ .filter((id) => id !== "default" && id !== "__esModule");
206
+ }
207
+
208
+ const propMatches = [...source.matchAll(/exports\.(\w+)\s*=/g)].map(
209
+ (m) => m[1],
210
+ );
211
+ return [...new Set(propMatches)].filter(
212
+ (id) => id !== "default" && id !== "__esModule",
213
+ );
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Post-processing transforms
218
+ // ---------------------------------------------------------------------------
219
+
220
+ // esbuild emits __require("ext") for CJS externals in ESM — rewrite to proper import declarations
221
+ function rewriteExternalRequireCalls(code: string): string {
222
+ const specifiers = new Set<string>();
223
+ for (const m of code.matchAll(/__require\(["']([^"']+)["']\)/g)) {
224
+ specifiers.add(m[1]);
225
+ }
226
+ if (specifiers.size === 0) return code;
227
+
228
+ const uniqueSpecs = [...specifiers];
229
+ const importBlock = uniqueSpecs
230
+ .map((spec, idx) => `import * as __imported${idx} from "${spec}";`)
231
+ .join("\n");
232
+
233
+ let patched = code;
234
+ for (let idx = 0; idx < uniqueSpecs.length; idx++) {
235
+ const spec = uniqueSpecs[idx];
236
+ patched = patched.split(`__require("${spec}")`).join(`__imported${idx}`);
237
+ patched = patched.split(`__require('${spec}')`).join(`__imported${idx}`);
238
+ }
239
+
240
+ return importBlock + "\n" + patched;
241
+ }
242
+
243
+ // esbuild wraps CJS as `export default require_xxx()` with no named exports.
244
+ // When we know the names from static analysis, inject explicit re-exports.
245
+ function injectNamedExports(code: string, names: string[]): string {
246
+ if (names.length === 0) return code;
247
+
248
+ const wrapperPattern = /export\s+default\s+(require_\w+)\(\)\s*;?/;
249
+ const m = code.match(wrapperPattern);
250
+ if (!m) return code;
251
+
252
+ const requireCall = m[1];
253
+
254
+ const replacement =
255
+ `var __mod = ${requireCall}();\nexport default __mod;\n` +
256
+ names.map((n) => `export var ${n} = __mod.${n};`).join("\n") +
257
+ "\n";
258
+
259
+ return code.replace(m[0], replacement);
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Class facade
264
+ // ---------------------------------------------------------------------------
265
+
266
+ export class BrowserBundler {
267
+ private vol: MemoryVolume;
268
+
269
+ constructor(vol: MemoryVolume, options?: { external?: string[] }) {
270
+ this.vol = vol;
271
+ attachVolume(vol);
272
+ if (options?.external) setExternalPackages(options.external);
273
+ }
274
+
275
+ bundle(specifier: string): Promise<string> {
276
+ return bundleForBrowser(specifier);
277
+ }
278
+
279
+ clearCache(): void {
280
+ invalidateBundleCache();
281
+ }
282
+ }
283
+
284
+ export default BrowserBundler;
@@ -0,0 +1,396 @@
1
+ // Dependency Installer
2
+ // Handles the full install lifecycle: resolve, download, extract, transform, bin stubs, lock file.
3
+
4
+ import { MemoryVolume } from "../memory-volume";
5
+ import { RegistryClient, RegistryConfig } from "./registry-client";
6
+ import {
7
+ resolveDependencyTree,
8
+ resolveFromManifest,
9
+ ResolvedDependency,
10
+ ResolutionConfig,
11
+ } from "./version-resolver";
12
+ import { downloadAndExtract } from "./archive-extractor";
13
+ import { convertPackage, prepareTransformer } from "../module-transformer";
14
+ import type { PackageManifest } from "../types/manifest";
15
+ import * as path from "../polyfills/path";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Public types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface InstallFlags {
22
+ registry?: string;
23
+ persist?: boolean;
24
+ persistDev?: boolean;
25
+ withDevDeps?: boolean;
26
+ withOptionalDeps?: boolean;
27
+ onProgress?: (message: string) => void;
28
+ // default: true
29
+ transformModules?: boolean;
30
+ }
31
+
32
+ export interface InstallOutcome {
33
+ resolved: Map<string, ResolvedDependency>;
34
+ newPackages: string[];
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ // Normalize bin field — handles both shorthand string and object forms
42
+ function normalizeBinField(
43
+ packageName: string,
44
+ bin?: string | Record<string, string>,
45
+ ): Record<string, string> {
46
+ if (!bin) return {};
47
+ if (typeof bin === "string") {
48
+ const command = packageName.includes("/")
49
+ ? packageName.split("/").pop()!
50
+ : packageName;
51
+ return { [command]: bin };
52
+ }
53
+ return bin;
54
+ }
55
+
56
+ // Split "express@4.18.2" or "@types/node@20" into name + version
57
+ function splitSpecifier(spec: string): { name: string; version?: string } {
58
+ if (spec.startsWith("@")) {
59
+ const slashIdx = spec.indexOf("/");
60
+ if (slashIdx === -1)
61
+ throw new Error(`Malformed package specifier: ${spec}`);
62
+
63
+ const tail = spec.slice(slashIdx + 1);
64
+ const atIdx = tail.indexOf("@");
65
+ if (atIdx === -1) return { name: spec };
66
+ return {
67
+ name: spec.slice(0, slashIdx + 1 + atIdx),
68
+ version: tail.slice(atIdx + 1),
69
+ };
70
+ }
71
+
72
+ const atIdx = spec.indexOf("@");
73
+ if (atIdx === -1) return { name: spec };
74
+ return {
75
+ name: spec.slice(0, atIdx),
76
+ version: spec.slice(atIdx + 1),
77
+ };
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Main class
82
+ // ---------------------------------------------------------------------------
83
+
84
+ let transformerReady = false;
85
+
86
+ export class DependencyInstaller {
87
+ private vol: MemoryVolume;
88
+ private registryClient: RegistryClient;
89
+ private workingDir: string;
90
+
91
+ constructor(vol: MemoryVolume, opts: { cwd?: string } & RegistryConfig = {}) {
92
+ this.vol = vol;
93
+ this.registryClient = new RegistryClient(opts);
94
+ this.workingDir = opts.cwd || "/";
95
+ }
96
+
97
+ // -----------------------------------------------------------------------
98
+ // Public API
99
+ // -----------------------------------------------------------------------
100
+
101
+ async install(
102
+ packageName: string,
103
+ version?: string,
104
+ flags: InstallFlags = {},
105
+ ): Promise<InstallOutcome> {
106
+ const { onProgress } = flags;
107
+
108
+ const spec = splitSpecifier(packageName);
109
+ const targetName = spec.name;
110
+ const targetRange = version || spec.version || "latest";
111
+
112
+ onProgress?.(`Resolving ${targetName}@${targetRange}...`);
113
+
114
+ const resolutionOpts: ResolutionConfig = {
115
+ registry: this.registryClient,
116
+ devDependencies: flags.withDevDeps,
117
+ optionalDependencies: flags.withOptionalDeps,
118
+ onProgress,
119
+ };
120
+
121
+ const tree = await resolveDependencyTree(
122
+ targetName,
123
+ targetRange,
124
+ resolutionOpts,
125
+ );
126
+
127
+ const newPkgs = await this.materializePackages(tree, flags);
128
+
129
+ if (flags.persist || flags.persistDev) {
130
+ const entry = tree.get(targetName);
131
+ if (entry) {
132
+ await this.patchManifest(
133
+ targetName,
134
+ `^${entry.version}`,
135
+ !!flags.persistDev,
136
+ );
137
+ }
138
+ }
139
+
140
+ onProgress?.(`Installed ${tree.size} package(s)`);
141
+
142
+ return { resolved: tree, newPackages: newPkgs };
143
+ }
144
+
145
+ async installFromManifest(
146
+ manifestPath?: string,
147
+ flags: InstallFlags = {},
148
+ ): Promise<InstallOutcome> {
149
+ const { onProgress } = flags;
150
+
151
+ const jsonPath = manifestPath || path.join(this.workingDir, "package.json");
152
+
153
+ if (!this.vol.existsSync(jsonPath)) {
154
+ throw new Error(`Manifest not found at ${jsonPath}`);
155
+ }
156
+
157
+ const raw = this.vol.readFileSync(jsonPath, "utf8");
158
+ const manifest: PackageManifest = JSON.parse(raw);
159
+
160
+ onProgress?.("Resolving dependency tree...");
161
+
162
+ const resolutionOpts: ResolutionConfig = {
163
+ registry: this.registryClient,
164
+ devDependencies: flags.withDevDeps,
165
+ optionalDependencies: flags.withOptionalDeps,
166
+ onProgress,
167
+ };
168
+
169
+ const tree = await resolveFromManifest(manifest, resolutionOpts);
170
+
171
+ const newPkgs = await this.materializePackages(tree, flags);
172
+
173
+ onProgress?.(`Installed ${tree.size} package(s)`);
174
+
175
+ return { resolved: tree, newPackages: newPkgs };
176
+ }
177
+
178
+ listInstalled(): Record<string, string> {
179
+ const nmDir = path.join(this.workingDir, "node_modules");
180
+ if (!this.vol.existsSync(nmDir)) return {};
181
+
182
+ const result: Record<string, string> = {};
183
+ const topLevel = this.vol.readdirSync(nmDir) as string[];
184
+
185
+ for (const entry of topLevel) {
186
+ if (entry.startsWith(".")) continue;
187
+
188
+ if (entry.startsWith("@")) {
189
+ const scopeDir = path.join(nmDir, entry);
190
+ const scopedEntries = this.vol.readdirSync(scopeDir) as string[];
191
+ for (const child of scopedEntries) {
192
+ const manifest = path.join(scopeDir, child, "package.json");
193
+ if (this.vol.existsSync(manifest)) {
194
+ const data = JSON.parse(this.vol.readFileSync(manifest, "utf8"));
195
+ result[`${entry}/${child}`] = data.version;
196
+ }
197
+ }
198
+ } else {
199
+ const manifest = path.join(nmDir, entry, "package.json");
200
+ if (this.vol.existsSync(manifest)) {
201
+ const data = JSON.parse(this.vol.readFileSync(manifest, "utf8"));
202
+ result[entry] = data.version;
203
+ }
204
+ }
205
+ }
206
+
207
+ return result;
208
+ }
209
+
210
+ // -----------------------------------------------------------------------
211
+ // Private helpers
212
+ // -----------------------------------------------------------------------
213
+
214
+ // Download, extract, transform, and wire up packages not already in node_modules
215
+ private async materializePackages(
216
+ tree: Map<string, ResolvedDependency>,
217
+ flags: InstallFlags,
218
+ ): Promise<string[]> {
219
+ const { onProgress } = flags;
220
+ const additions: string[] = [];
221
+
222
+ const nmRoot = path.join(this.workingDir, "node_modules");
223
+ this.vol.mkdirSync(nmRoot, { recursive: true });
224
+
225
+ const pending: Array<{
226
+ depName: string;
227
+ dep: ResolvedDependency;
228
+ targetDir: string;
229
+ }> = [];
230
+
231
+ for (const [depName, dep] of tree) {
232
+ const targetDir = path.join(nmRoot, depName);
233
+ const existingManifest = path.join(targetDir, "package.json");
234
+
235
+ if (this.vol.existsSync(existingManifest)) {
236
+ try {
237
+ const current = JSON.parse(
238
+ this.vol.readFileSync(existingManifest, "utf8"),
239
+ );
240
+ if (current.version === dep.version) {
241
+ onProgress?.(`Skipping ${depName}@${dep.version} (up to date)`);
242
+ continue;
243
+ }
244
+ } catch {
245
+ // corrupt manifest, reinstall
246
+ }
247
+ }
248
+
249
+ pending.push({ depName, dep, targetDir });
250
+ }
251
+
252
+ // Only need main-thread transformer as fallback when workers aren't available
253
+ const shouldTransform = flags.transformModules !== false;
254
+ if (shouldTransform && !transformerReady) {
255
+ if (typeof Worker === "undefined") {
256
+ onProgress?.("Preparing module transformer...");
257
+ await prepareTransformer();
258
+ }
259
+ transformerReady = true;
260
+ }
261
+
262
+ // Safe to batch aggressively since extract + transform are offloaded to workers
263
+ const WORKER_COUNT = 12;
264
+ onProgress?.(`Downloading ${pending.length} package(s)...`);
265
+
266
+ for (let offset = 0; offset < pending.length; offset += WORKER_COUNT) {
267
+ const batch = pending.slice(offset, offset + WORKER_COUNT);
268
+
269
+ await Promise.all(
270
+ batch.map(async ({ depName, dep, targetDir }) => {
271
+ onProgress?.(` Fetching ${depName}@${dep.version}...`);
272
+
273
+ await downloadAndExtract(dep.tarballUrl, this.vol, targetDir, {
274
+ stripComponents: 1,
275
+ });
276
+
277
+ if (shouldTransform) {
278
+ try {
279
+ const transformed = await convertPackage(
280
+ this.vol,
281
+ targetDir,
282
+ onProgress,
283
+ );
284
+ if (transformed > 0) {
285
+ onProgress?.(
286
+ ` Transformed ${transformed} file(s) in ${depName}`,
287
+ );
288
+ }
289
+ } catch (err) {
290
+ onProgress?.(
291
+ ` Warning: transformation failed for ${depName}: ${err}`,
292
+ );
293
+ }
294
+ }
295
+
296
+ this.createBinStubs(nmRoot, depName, targetDir);
297
+
298
+ additions.push(depName);
299
+ }),
300
+ );
301
+ }
302
+
303
+ this.writeLockFile(tree);
304
+
305
+ return additions;
306
+ }
307
+
308
+ private createBinStubs(
309
+ nmRoot: string,
310
+ depName: string,
311
+ pkgDir: string,
312
+ ): void {
313
+ try {
314
+ const manifestPath = path.join(pkgDir, "package.json");
315
+ if (!this.vol.existsSync(manifestPath)) return;
316
+
317
+ const data = JSON.parse(this.vol.readFileSync(manifestPath, "utf8"));
318
+ const bins = normalizeBinField(depName, data.bin);
319
+ const binDir = path.join(nmRoot, ".bin");
320
+
321
+ for (const [cmd, relPath] of Object.entries(bins)) {
322
+ this.vol.mkdirSync(binDir, { recursive: true });
323
+ const target = path.join(pkgDir, relPath);
324
+ this.vol.writeFileSync(
325
+ path.join(binDir, cmd),
326
+ `node "${target}" "$@"\n`,
327
+ );
328
+ }
329
+ } catch {
330
+ // best-effort
331
+ }
332
+ }
333
+
334
+ private writeLockFile(tree: Map<string, ResolvedDependency>): void {
335
+ const entries: Record<string, { version: string; resolved: string }> = {};
336
+
337
+ for (const [depName, dep] of tree) {
338
+ entries[depName] = {
339
+ version: dep.version,
340
+ resolved: dep.tarballUrl,
341
+ };
342
+ }
343
+
344
+ const lockPath = path.join(
345
+ this.workingDir,
346
+ "node_modules",
347
+ ".package-lock.json",
348
+ );
349
+ this.vol.writeFileSync(lockPath, JSON.stringify(entries, null, 2));
350
+ }
351
+
352
+ private async patchManifest(
353
+ depName: string,
354
+ versionSpec: string,
355
+ asDev: boolean,
356
+ ): Promise<void> {
357
+ const jsonPath = path.join(this.workingDir, "package.json");
358
+
359
+ let manifest: Record<string, unknown> = {};
360
+ if (this.vol.existsSync(jsonPath)) {
361
+ manifest = JSON.parse(this.vol.readFileSync(jsonPath, "utf8"));
362
+ }
363
+
364
+ const section = asDev ? "devDependencies" : "dependencies";
365
+ if (!manifest[section]) {
366
+ manifest[section] = {};
367
+ }
368
+ (manifest[section] as Record<string, string>)[depName] = versionSpec;
369
+
370
+ this.vol.writeFileSync(jsonPath, JSON.stringify(manifest, null, 2));
371
+ }
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Convenience function
376
+ // ---------------------------------------------------------------------------
377
+
378
+ // One-shot install: `install("express@4.18.2", vol)`
379
+ export async function install(
380
+ specifier: string,
381
+ vol: MemoryVolume,
382
+ flags?: InstallFlags,
383
+ ): Promise<InstallOutcome> {
384
+ const installer = new DependencyInstaller(vol);
385
+ return installer.install(specifier, undefined, flags);
386
+ }
387
+
388
+ export { RegistryClient } from "./registry-client";
389
+ export type {
390
+ RegistryConfig,
391
+ VersionDetail,
392
+ PackageMetadata,
393
+ } from "./registry-client";
394
+ export type { ResolvedDependency, ResolutionConfig } from "./version-resolver";
395
+ export type { ExtractionOptions } from "./archive-extractor";
396
+ export { splitSpecifier };