@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,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 };
|