@nimbus-sh/worker 0.9.0 → 0.11.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/NOTICE.md +5 -0
- package/README.md +10 -0
- package/dist/_shared/session-router.d.ts +7 -0
- package/dist/_shared/session-router.d.ts.map +1 -1
- package/dist/_shared/session-router.js +7 -0
- package/dist/bindings/body.d.ts +6 -0
- package/dist/bindings/body.d.ts.map +1 -0
- package/dist/bindings/body.js +37 -0
- package/dist/bindings/kv.d.ts +0 -2
- package/dist/bindings/kv.d.ts.map +1 -1
- package/dist/bindings/kv.js +3 -42
- package/dist/bindings/r2.d.ts +0 -2
- package/dist/bindings/r2.d.ts.map +1 -1
- package/dist/bindings/r2.js +4 -44
- package/dist/esbuild-cli-artifact.generated.d.ts +18 -0
- package/dist/esbuild-cli-artifact.generated.d.ts.map +1 -0
- package/dist/esbuild-cli-artifact.generated.js +17 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts +24 -24
- package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -1
- package/dist/esbuild-wasm-bundle.generated.js +24 -24
- package/dist/facets/cirrus-real.d.ts.map +1 -1
- package/dist/facets/cirrus-real.js +11 -6
- package/dist/facets/compose.d.ts +11 -7
- package/dist/facets/compose.d.ts.map +1 -1
- package/dist/facets/compose.js +8 -31
- package/dist/facets/data-plan.d.ts +79 -0
- package/dist/facets/data-plan.d.ts.map +1 -0
- package/dist/facets/data-plan.js +477 -0
- package/dist/facets/esbuild-bundle-pool.d.ts +1 -1
- package/dist/facets/esbuild-bundle-pool.js +3 -3
- package/dist/facets/esbuild-transform.d.ts +38 -8
- package/dist/facets/esbuild-transform.d.ts.map +1 -1
- package/dist/facets/esbuild-transform.js +219 -19
- package/dist/facets/exec-telemetry.d.ts +7 -3
- package/dist/facets/exec-telemetry.d.ts.map +1 -1
- package/dist/facets/manager.d.ts +198 -69
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +1276 -652
- package/dist/facets/opencode-staging.d.ts +0 -2
- package/dist/facets/opencode-staging.d.ts.map +1 -1
- package/dist/facets/opencode-staging.js +5 -11
- package/dist/facets/process.d.ts +4 -0
- package/dist/facets/process.d.ts.map +1 -1
- package/dist/facets/process.js +3 -0
- package/dist/facets/read-profile.d.ts +149 -0
- package/dist/facets/read-profile.d.ts.map +1 -0
- package/dist/facets/read-profile.js +483 -0
- package/dist/facets/real-vite-hmr.d.ts +2 -0
- package/dist/facets/real-vite-hmr.d.ts.map +1 -1
- package/dist/facets/real-vite-hmr.js +3 -2
- package/dist/facets/resident-identity.d.ts +0 -1
- package/dist/facets/resident-identity.d.ts.map +1 -1
- package/dist/facets/resident-identity.js +0 -3
- package/dist/facets/wasm-swap-registry.d.ts +34 -25
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
- package/dist/facets/wasm-swap-registry.js +62 -56
- package/dist/facets/ws-terminal.d.ts +4 -4
- package/dist/facets/ws-terminal.d.ts.map +1 -1
- package/dist/facets/ws-terminal.js +4 -5
- package/dist/git/commands.d.ts +33 -10
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +1891 -197
- package/dist/git/network-facet.d.ts +12 -2
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +69 -18
- package/dist/git/unified-diff.d.ts +74 -0
- package/dist/git/unified-diff.d.ts.map +1 -0
- package/dist/git/unified-diff.js +676 -0
- package/dist/git-bundle.generated.d.ts +7 -11
- package/dist/git-bundle.generated.d.ts.map +1 -1
- package/dist/git-bundle.generated.js +7 -11
- package/dist/hosted/commands.d.ts.map +1 -1
- package/dist/hosted/commands.js +64 -245
- package/dist/hosted/runtime.d.ts +22 -15
- package/dist/hosted/runtime.d.ts.map +1 -1
- package/dist/hosted/runtime.js +72 -14
- package/dist/hosted/services.d.ts +8 -1
- package/dist/hosted/services.d.ts.map +1 -1
- package/dist/hosted/services.js +12 -13
- package/dist/hosted/session.d.ts +165 -0
- package/dist/hosted/session.d.ts.map +1 -0
- package/dist/hosted/session.js +132 -0
- package/dist/loaders/generated-workers.d.ts +6 -3
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +8 -5
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
- package/dist/loaders/npm-resolve-preamble.js +0 -27
- package/dist/loaders/pre-bundle-preamble.d.ts +6 -3
- package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
- package/dist/loaders/pre-bundle-preamble.js +13 -8
- package/dist/node-shims-artifact.generated.d.ts +13 -4
- package/dist/node-shims-artifact.generated.d.ts.map +1 -1
- package/dist/node-shims-artifact.generated.js +16 -7
- package/dist/npm/bin-links.d.ts +3 -1
- package/dist/npm/bin-links.d.ts.map +1 -1
- package/dist/npm/bin-links.js +32 -8
- package/dist/npm/cache.d.ts +11 -1
- package/dist/npm/cache.d.ts.map +1 -1
- package/dist/npm/cache.js +21 -7
- package/dist/npm/install-batch-facet.d.ts +4 -2
- package/dist/npm/install-batch-facet.d.ts.map +1 -1
- package/dist/npm/install-batch-facet.js +32 -18
- package/dist/npm/installer.d.ts +23 -3
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +330 -131
- package/dist/npm/npx-install.d.ts +3 -1
- package/dist/npm/npx-install.d.ts.map +1 -1
- package/dist/npm/npx-install.js +4 -1
- package/dist/npm/package-lock.d.ts +32 -0
- package/dist/npm/package-lock.d.ts.map +1 -0
- package/dist/npm/package-lock.js +65 -0
- package/dist/npm/placement.d.ts +16 -0
- package/dist/npm/placement.d.ts.map +1 -0
- package/dist/npm/placement.js +33 -0
- package/dist/npm/pre-bundle-facet.d.ts +1 -30
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
- package/dist/npm/pre-bundle-facet.js +3 -5
- package/dist/npm/r2-cache.d.ts +12 -6
- package/dist/npm/r2-cache.d.ts.map +1 -1
- package/dist/npm/r2-cache.js +34 -16
- package/dist/npm/resolve-one-facet.d.ts +3 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +15 -3
- package/dist/npm/resolver.d.ts +11 -13
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +12 -25
- package/dist/opencode-artifact.generated.js +2 -2
- package/dist/replica/routing.d.ts +2 -38
- package/dist/replica/routing.d.ts.map +1 -1
- package/dist/replica/routing.js +3 -55
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +15 -2
- package/dist/router/public-directory.d.ts +18 -0
- package/dist/router/public-directory.d.ts.map +1 -1
- package/dist/router/public-directory.js +1 -1
- package/dist/router/remote-api.d.ts +3 -1
- package/dist/router/remote-api.d.ts.map +1 -1
- package/dist/router/remote-api.js +42 -79
- package/dist/runtime/bash-repl.js +2 -2
- package/dist/runtime/bun-repl.d.ts.map +1 -1
- package/dist/runtime/bun-repl.js +2 -13
- package/dist/runtime/cpython-resident.d.ts.map +1 -1
- package/dist/runtime/cpython-resident.js +2 -10
- package/dist/runtime/esbuild-wasm-bytes.d.ts +37 -18
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/esbuild-wasm-bytes.js +66 -92
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +0 -7
- package/dist/runtime/git-bundle-artifact.d.ts +17 -0
- package/dist/runtime/git-bundle-artifact.d.ts.map +1 -0
- package/dist/runtime/git-bundle-artifact.js +25 -0
- package/dist/runtime/node-repl.d.ts.map +1 -1
- package/dist/runtime/node-repl.js +2 -13
- package/dist/runtime/node-shims-artifact.d.ts +22 -23
- package/dist/runtime/node-shims-artifact.d.ts.map +1 -1
- package/dist/runtime/node-shims-artifact.js +48 -78
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +2358 -996
- package/dist/runtime/opencode-artifact.d.ts.map +1 -1
- package/dist/runtime/opencode-artifact.js +17 -53
- package/dist/runtime/opencode-facet-runner.d.ts +3 -6
- package/dist/runtime/opencode-facet-runner.d.ts.map +1 -1
- package/dist/runtime/opencode-facet-runner.js +45 -10
- package/dist/runtime/opentui-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/opentui-wasm-bytes.js +16 -49
- package/dist/runtime/package-manager.d.ts +0 -2
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +0 -3
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +4 -35
- package/dist/runtime/repl-session.d.ts +10 -0
- package/dist/runtime/repl-session.d.ts.map +1 -1
- package/dist/runtime/repl-session.js +7 -0
- package/dist/runtime/ruby-repl.d.ts.map +1 -1
- package/dist/runtime/ruby-repl.js +2 -2
- package/dist/runtime/ruby-resident.d.ts.map +1 -1
- package/dist/runtime/ruby-resident.js +41 -51
- package/dist/runtime/runtime-catalog.d.ts +10 -3
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +125 -15
- package/dist/runtime/sqlite-wasm-bytes.d.ts +4 -4
- package/dist/runtime/sqlite-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/sqlite-wasm-bytes.js +21 -57
- package/dist/runtime/staged-source.d.ts +67 -0
- package/dist/runtime/staged-source.d.ts.map +1 -0
- package/dist/runtime/staged-source.js +113 -0
- package/dist/runtime/wasi/preamble.d.ts +0 -22
- package/dist/runtime/wasi/preamble.d.ts.map +1 -1
- package/dist/runtime/wasi/preamble.js +98 -1425
- package/dist/runtime-catalog.generated.js +1 -1
- package/dist/session/agent.d.ts +14 -2
- package/dist/session/agent.d.ts.map +1 -1
- package/dist/session/agent.js +57 -19
- package/dist/session/ai.d.ts +0 -11
- package/dist/session/ai.d.ts.map +1 -1
- package/dist/session/ai.js +0 -4
- package/dist/session/fs-watch.d.ts +0 -11
- package/dist/session/fs-watch.d.ts.map +1 -1
- package/dist/session/fs-watch.js +0 -21
- package/dist/session/hibernation.d.ts +68 -2
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +95 -1
- package/dist/session/init-phases.d.ts +1 -2
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +1 -2
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +7 -20
- package/dist/session/keys.d.ts +0 -3
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +0 -3
- package/dist/session/legacy-reset.d.ts +11 -0
- package/dist/session/legacy-reset.d.ts.map +1 -0
- package/dist/session/legacy-reset.js +16 -0
- package/dist/session/nimbus-session.d.ts +50 -16
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +77 -71
- package/dist/session/npm-install-port.d.ts.map +1 -1
- package/dist/session/npm-install-port.js +2 -0
- package/dist/session/programmatic.d.ts +13 -18
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +53 -39
- package/dist/session/replica-routes.d.ts +2 -3
- package/dist/session/replica-routes.d.ts.map +1 -1
- package/dist/session/replica-routes.js +3 -10
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +11 -12
- package/dist/session/rpc.d.ts +108 -21
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +194 -39
- package/dist/session/start-real-vite.js +2 -2
- package/dist/session/state-store.d.ts +1 -17
- package/dist/session/state-store.d.ts.map +1 -1
- package/dist/session/state-store.js +3 -53
- package/dist/session/supervisor-op.d.ts +7 -0
- package/dist/session/supervisor-op.d.ts.map +1 -1
- package/dist/session/supervisor-op.js +6 -1
- package/dist/session/supervisor-rpc.d.ts +116 -19
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +194 -84
- package/dist/session/vite-command.d.ts.map +1 -1
- package/dist/session/vite-command.js +17 -14
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +20 -8
- package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
- package/dist/shell/npm-bin-entrypoints.js +30 -5
- package/dist/vfs/facet-resident-limits.d.ts +33 -0
- package/dist/vfs/facet-resident-limits.d.ts.map +1 -0
- package/dist/vfs/facet-resident-limits.js +33 -0
- package/dist/vfs/facet-resident-store.d.ts +2 -10
- package/dist/vfs/facet-resident-store.d.ts.map +1 -1
- package/dist/vfs/facet-resident-store.js +1594 -219
- package/dist/workspace-host.d.ts +3 -1
- package/dist/workspace-host.d.ts.map +1 -1
- package/dist/workspace-host.js +2 -0
- package/dist/wrangler/nimbus-wrangler.d.ts +0 -2
- package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -1
- package/dist/wrangler/nimbus-wrangler.js +8 -22
- package/package.json +34 -7
- package/public/_assets/esbuild-0.24.2.js +2380 -0
- package/public/_assets/opencode/1.16.2/index-attach.js +2 -2
- package/public/_assets/runtime/esbuild-cli-c4fda380d52befc1.js +6892 -0
- package/public/_assets/runtime/git-de3e60c9f3ff006a.js +103 -0
- package/public/_assets/runtime/{node-shims-acaa037e6d26ec0d.js → node-shims-06565b121555c194.js} +2879 -992
- package/public/_assets/runtime/resident-store-6ffb1939c53d982f.js +2264 -0
- package/public/_assets/runtime/vfs-write-ledger-e501e8fc793bc2c1.js +889 -0
- package/scripts/build-opencode-attach-entry.mjs +51 -1
- package/scripts/bundle-esbuild-wasm.mjs +55 -39
- package/scripts/bundle-facet-workers.mjs +159 -10
- package/scripts/bundle-git.mjs +88 -27
- package/scripts/bundle-node-shims.mjs +72 -46
- package/scripts/bundle-runtime.mjs +18 -421
- package/scripts/cf-git-patch.mjs +137 -0
- package/scripts/patch-install-deps.mjs +49 -96
- package/scripts/runtime-specs.mjs +435 -0
- package/vendor/git-http.generated.d.ts +82 -0
- package/vendor/git-upstream.generated.d.ts +4378 -0
- package/vendor/git.LICENSE +7 -0
- package/vendor/git.generated.d.mts +21 -0
- package/vendor/git.generated.mjs +103 -0
- package/dist/replica/suspension.d.ts +0 -36
- package/dist/replica/suspension.d.ts.map +0 -1
- package/dist/replica/suspension.js +0 -49
- package/dist/runtime/static-server.d.ts +0 -3
- package/dist/runtime/static-server.d.ts.map +0 -1
- package/dist/runtime/static-server.js +0 -133
- package/scripts/clean-dist.mjs +0 -7
package/dist/npm/installer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* npm
|
|
2
|
+
* npm/installer.ts — Unified npm installer for Nimbus.
|
|
3
3
|
*
|
|
4
4
|
* Designed for bun/pnpm-level performance on Cloudflare DO + SQLite.
|
|
5
5
|
*
|
|
@@ -22,11 +22,13 @@
|
|
|
22
22
|
import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
23
23
|
import { BUNDLER_VERSION } from '@nimbus-sh/core/runtime/esbuild-service.js';
|
|
24
24
|
import { NpmCache } from './cache.js';
|
|
25
|
-
import { computeHoistPlan, } from './resolver.js';
|
|
26
|
-
import {
|
|
25
|
+
import { computeHoistPlan, hoistPlacements, } from './resolver.js';
|
|
26
|
+
import { nestedPlacement, visiblePlacements } from './placement.js';
|
|
27
|
+
import { packageLockMismatches, parsePackageLock, stringList, stringRecord } from './package-lock.js';
|
|
28
|
+
import { npmRegistryOrigin, packumentUrl } from './r2-cache.js';
|
|
27
29
|
import { satisfiesRange, isSemverRange } from './semver.js';
|
|
28
30
|
import { npmAddedLine, npmHttpCacheLine, npmHttpFetchLine, npmTitleLine, } from '@nimbus-sh/core/substrate/lifo/commands/system/npm-log.js';
|
|
29
|
-
import { applySwaps, findRejects, lookupSwap, isOptionalNativeBinding, formatSwapNotice, emitRegistryEvent, } from '../facets/wasm-swap-registry.js';
|
|
31
|
+
import { applySwaps, findRejects, lookupSwap, lookupReject, isOptionalNativeBinding, lookupStagedArtifact, applyStagedArtifact, policyNativePlatformReject, PACKAGE_ABI_POLICY, formatSwapNotice, emitRegistryEvent, } from '../facets/wasm-swap-registry.js';
|
|
30
32
|
import { resolvePackageEntry } from '@nimbus-sh/core/_shared/exports-resolver.js';
|
|
31
33
|
import { encodeWriteBatchStream } from '@nimbus-sh/platform/w7-frame.js';
|
|
32
34
|
import { Fanout, IN_DO_THRESHOLD } from '@nimbus-sh/fabric/fanout.js';
|
|
@@ -115,6 +117,7 @@ export class NpmInstaller {
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
async _installInner(projDir, nmDir, opts, log, start) {
|
|
120
|
+
const registry = npmRegistryOrigin(opts?.registry);
|
|
118
121
|
const phases = {};
|
|
119
122
|
const installed = [];
|
|
120
123
|
const failed = [];
|
|
@@ -125,7 +128,10 @@ export class NpmInstaller {
|
|
|
125
128
|
this.npmLog('verbose', npmTitleLine(opts?.packages ?? []));
|
|
126
129
|
let phaseStart = Date.now();
|
|
127
130
|
log('Checking lockfile...');
|
|
128
|
-
|
|
131
|
+
// npm ci takes its roots from the lock, not from package.json specs.
|
|
132
|
+
const { specs, devOnly, optionalRoots, advised } = opts?.fromLockfile
|
|
133
|
+
? { specs: {}, devOnly: new Set(), optionalRoots: new Set(), advised: new Set() }
|
|
134
|
+
: await this.buildSpecs(projDir, opts?.packages, opts?.production);
|
|
129
135
|
// EBADPLATFORM refusals (os/cpu/libc allowlists on a required
|
|
130
136
|
// package) fail the install like real npm; every refused name was
|
|
131
137
|
// already logged as a [skip] line, name them again with the reason.
|
|
@@ -137,16 +143,30 @@ export class NpmInstaller {
|
|
|
137
143
|
log(`npm ERR! ${r.name}: ${r.reason}`);
|
|
138
144
|
}
|
|
139
145
|
};
|
|
140
|
-
|
|
146
|
+
const packageLock = opts?.fromLockfile
|
|
147
|
+
? await this.treeFromPackageLock(projDir, opts.production === true, log, registry)
|
|
148
|
+
: null;
|
|
149
|
+
if (packageLock === null && Object.keys(specs).length === 0) {
|
|
141
150
|
log('No dependencies to install.');
|
|
142
151
|
return { installed, failed, totalFiles: 0, elapsed: Date.now() - start, cachedHits: 0, phases: {} };
|
|
143
152
|
}
|
|
144
153
|
const lockfile = this.cache.readLockfile(projDir);
|
|
145
154
|
let resolved;
|
|
155
|
+
let nested;
|
|
146
156
|
let usedLockfile = false;
|
|
147
|
-
if (
|
|
157
|
+
if (packageLock !== null) {
|
|
158
|
+
({ resolved, nested } = packageLock);
|
|
159
|
+
recordRefusals(packageLock.rejected);
|
|
160
|
+
for (const [name, reason] of packageLock.unresolved) {
|
|
161
|
+
failed.push(name);
|
|
162
|
+
log(`npm ERR! could not resolve ${name}: ${reason}`);
|
|
163
|
+
}
|
|
164
|
+
log(`Lockfile ${packageLock.lockName}: ${resolved.size + nested.size} packages to place.`);
|
|
165
|
+
phases['lock-check'] = Date.now() - phaseStart;
|
|
166
|
+
}
|
|
167
|
+
else if (lockfile && !opts?.packages && this.isLockfileValid(lockfile, specs)) {
|
|
148
168
|
log(`Lockfile valid (${lockfile.size} packages). Skipping resolution.`);
|
|
149
|
-
resolved = this.lockfileToResolved(lockfile);
|
|
169
|
+
({ resolved, nested } = this.lockfileToResolved(lockfile));
|
|
150
170
|
usedLockfile = true;
|
|
151
171
|
phases['lock-check'] = Date.now() - phaseStart;
|
|
152
172
|
}
|
|
@@ -166,8 +186,9 @@ export class NpmInstaller {
|
|
|
166
186
|
phaseStart = Date.now();
|
|
167
187
|
setInstallPhase('resolve');
|
|
168
188
|
log(`Resolving ${Object.keys(specs).length} dependencies (path: fanout, fetch: ${this.fetchFn ? 'facet-proxy' : 'global'})...`);
|
|
169
|
-
const tree = await this.resolveTreeViaFanout(specs, log, { optionalRoots, devOnly, advised });
|
|
189
|
+
const tree = await this.resolveTreeViaFanout(specs, log, { optionalRoots, devOnly, advised, registry });
|
|
170
190
|
resolved = tree.resolved;
|
|
191
|
+
nested = tree.nested;
|
|
171
192
|
phases['resolve'] = Date.now() - phaseStart;
|
|
172
193
|
// Transitive platform-gate refusals. Required ones join `failed`
|
|
173
194
|
// with their reason; optional-edge ones stay out of the outcome
|
|
@@ -197,13 +218,15 @@ export class NpmInstaller {
|
|
|
197
218
|
// ── Phase 2: Hoist ────────────────────────────────────────────────
|
|
198
219
|
phaseStart = Date.now();
|
|
199
220
|
setInstallPhase('hoist');
|
|
200
|
-
const hoistPlan = computeHoistPlan(resolved);
|
|
221
|
+
const hoistPlan = computeHoistPlan(resolved, nested);
|
|
201
222
|
phases['hoist'] = Date.now() - phaseStart;
|
|
202
223
|
// ── Phase 3: Diff (cache check) ─────────────────────────────────
|
|
224
|
+
// Per placement: a nested copy is checked at its own directory.
|
|
203
225
|
phaseStart = Date.now();
|
|
204
226
|
setInstallPhase('diff');
|
|
205
227
|
const toFetch = [];
|
|
206
|
-
for (const
|
|
228
|
+
for (const target of hoistPlacements(hoistPlan)) {
|
|
229
|
+
const { pkg } = target;
|
|
207
230
|
if (!pkg.tarballUrl) {
|
|
208
231
|
// Resolved metadata with no tarball cannot be installed. Silently
|
|
209
232
|
// dropping it here left the package absent from node_modules and
|
|
@@ -213,7 +236,7 @@ export class NpmInstaller {
|
|
|
213
236
|
continue;
|
|
214
237
|
}
|
|
215
238
|
// Check if already installed at the correct path
|
|
216
|
-
const pkgJsonPath = nmDir + '/' +
|
|
239
|
+
const pkgJsonPath = nmDir + '/' + target.placement + '/package.json';
|
|
217
240
|
if (this.vfs.exists(pkgJsonPath)) {
|
|
218
241
|
try {
|
|
219
242
|
const existing = JSON.parse(this.vfs.readFileString(pkgJsonPath));
|
|
@@ -229,7 +252,7 @@ export class NpmInstaller {
|
|
|
229
252
|
// Not present in the VFS at the right version — fetch it. The batch
|
|
230
253
|
// facet consults the shared L2 (caches.default) + L3 (R2) tarball
|
|
231
254
|
// tiers itself, so a cross-DO warm cache still lands here.
|
|
232
|
-
toFetch.push(
|
|
255
|
+
toFetch.push(target);
|
|
233
256
|
}
|
|
234
257
|
phases['diff'] = Date.now() - phaseStart;
|
|
235
258
|
log(`To fetch: ${toFetch.length}, already installed: ${cachedHits}`);
|
|
@@ -257,7 +280,7 @@ export class NpmInstaller {
|
|
|
257
280
|
const commitsBefore = this.storageCommitCount();
|
|
258
281
|
if (toFetch.length > 0) {
|
|
259
282
|
log(`Fetching ${toFetch.length} packages... (path: batch-facet)`);
|
|
260
|
-
const batchResult = await this.fetchViaBatchFacet(toFetch,
|
|
283
|
+
const batchResult = await this.fetchViaBatchFacet(toFetch, nmDir, opts?.pid);
|
|
261
284
|
totalFiles += batchResult.filesWritten;
|
|
262
285
|
for (const name of batchResult.installed)
|
|
263
286
|
installed.push(name);
|
|
@@ -273,7 +296,7 @@ export class NpmInstaller {
|
|
|
273
296
|
phases['link-bins'] = Date.now() - phaseStart;
|
|
274
297
|
// ── Write lockfile ──────────────────────────────────────────────
|
|
275
298
|
if (!usedLockfile || opts?.packages) {
|
|
276
|
-
this.writeLockfile(projDir,
|
|
299
|
+
this.writeLockfile(projDir, hoistPlan, nmDir);
|
|
277
300
|
}
|
|
278
301
|
// ── Update package.json if explicit packages were added ─────────
|
|
279
302
|
if (opts?.packages && opts.packages.length > 0) {
|
|
@@ -400,7 +423,8 @@ export class NpmInstaller {
|
|
|
400
423
|
`${failed.length} missing: ${failed.join(', ')}`);
|
|
401
424
|
}
|
|
402
425
|
else {
|
|
403
|
-
|
|
426
|
+
const nestedNote = hoistPlan.nested.size > 0 ? ` (${hoistPlan.nested.size} nested)` : '';
|
|
427
|
+
log(`Done! ${installed.length} packages${nestedNote}, ${totalFiles} files in ${(elapsed / 1000).toFixed(1)}s`);
|
|
404
428
|
// npm's own summary line, unstyled: the styled one the command site
|
|
405
429
|
// prints carries a colour prefix that no log parser matches. Carried
|
|
406
430
|
// at `http` — the level from which this stream is machine-readable —
|
|
@@ -442,8 +466,13 @@ export class NpmInstaller {
|
|
|
442
466
|
* width < IN_DO_THRESHOLD (5) → in-DO fanout in-DO loader-pool
|
|
443
467
|
* width >= IN_DO_THRESHOLD → peer-DO fanout peer-DO (sibling NimbusSession DOs)
|
|
444
468
|
*
|
|
469
|
+
* Resolution is per edge, not per name: the first version of a name goes
|
|
470
|
+
* to root; a later edge the nearest visible placement does not satisfy
|
|
471
|
+
* nests a copy under its dependent, never higher (placement.ts; live case
|
|
472
|
+
* in tests/unit/npm-install-nested-conflict.mjs).
|
|
473
|
+
*
|
|
445
474
|
* The supervisor still owns:
|
|
446
|
-
* -
|
|
475
|
+
* - placement (`placed` / `pending` / `settled`, by placement path),
|
|
447
476
|
* - X.5-F top-level / required-peer policy,
|
|
448
477
|
* - X.5-G G1 optional-native silent-skip,
|
|
449
478
|
* - X.5-drizzle best-effort tagging on optional-peer subtrees,
|
|
@@ -465,6 +494,7 @@ export class NpmInstaller {
|
|
|
465
494
|
*/
|
|
466
495
|
async resolveTreeViaFanout(specs, log, opts = {}) {
|
|
467
496
|
const t0 = Date.now();
|
|
497
|
+
const registry = npmRegistryOrigin(opts.registry);
|
|
468
498
|
const optionalRoots = opts.optionalRoots ?? new Set();
|
|
469
499
|
const devOnly = opts.devOnly ?? new Set();
|
|
470
500
|
// Policy advisories already announced — one `note:` line per name
|
|
@@ -479,21 +509,27 @@ export class NpmInstaller {
|
|
|
479
509
|
log(`[npm] note: ${ev.from} has no Workers-compatible build${marker}: ${ev.reason}${hint}`);
|
|
480
510
|
};
|
|
481
511
|
const __f2Diag = (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1');
|
|
482
|
-
// Per-walk state — supervisor side.
|
|
512
|
+
// Per-walk state — supervisor side. Root by name, nested by placement
|
|
513
|
+
// path, `placed` is both; `pending` is dispatched and unanswered,
|
|
514
|
+
// `settled` answered whatever the answer was.
|
|
483
515
|
const resolved = new Map();
|
|
516
|
+
const nested = new Map();
|
|
517
|
+
const placed = new Map();
|
|
518
|
+
const pending = new Set();
|
|
519
|
+
const settled = new Set();
|
|
484
520
|
// Dependencies the walk asked for and could not resolve, name → reason.
|
|
485
|
-
// A name lands here at most once: the frontier marks it `seen` before
|
|
486
|
-
// dispatch, so no later parent re-enqueues it.
|
|
487
521
|
const unresolved = new Map();
|
|
488
522
|
// EBADPLATFORM refusals (os/cpu/libc allowlists), in walk order.
|
|
489
523
|
// Announced as `[skip]` lines at record time; required-vs-optional is
|
|
490
524
|
// classified at end of walk, when every required edge has been seen.
|
|
491
525
|
const refusals = new Map();
|
|
492
|
-
const seen = new Set();
|
|
493
526
|
const topLevelNames = new Set(Object.keys(specs));
|
|
494
527
|
const optionalNames = new Set(); // X.5-G G1
|
|
495
528
|
const bestEffortNames = new Set(); // X.5-drizzle
|
|
496
|
-
|
|
529
|
+
// A cycle with mutually incompatible ranges has no finite layout; the
|
|
530
|
+
// edge is refused at this depth rather than nested without end.
|
|
531
|
+
const MAX_NEST_DEPTH = 16;
|
|
532
|
+
let queue = Object.entries(specs).map(([name, range]) => ({ name, range, from: '', kind: 'dep' }));
|
|
497
533
|
const cacheWritesPending = [];
|
|
498
534
|
let totalPackumentBytes = 0;
|
|
499
535
|
let totalPackumentsDecoded = 0;
|
|
@@ -544,21 +580,53 @@ export class NpmInstaller {
|
|
|
544
580
|
// Frontier loop. Each iteration = ONE BFS layer dispatched as ONE
|
|
545
581
|
// submitMany batch.
|
|
546
582
|
while (queue.length > 0) {
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
583
|
+
// Decide each edge against what Node's walk from its dependent sees.
|
|
584
|
+
// One whose nearest visible placement is in flight waits a layer.
|
|
585
|
+
// A copy nested under a package is decided here in the layer after
|
|
586
|
+
// that package resolves, before anything beneath it can resolve, so
|
|
587
|
+
// no descendant is ever decided against a copy it will later shadow.
|
|
550
588
|
const layer = [];
|
|
551
|
-
const
|
|
552
|
-
for (const
|
|
553
|
-
|
|
589
|
+
const deferred = [];
|
|
590
|
+
for (const edge of queue) {
|
|
591
|
+
const req = parseRegistryRequest(edge.name, edge.range);
|
|
592
|
+
let target = edge.name;
|
|
593
|
+
let decided = false;
|
|
594
|
+
for (const candidate of visiblePlacements(edge.from, edge.name)) {
|
|
595
|
+
if (pending.has(candidate)) {
|
|
596
|
+
deferred.push(edge);
|
|
597
|
+
decided = true;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
const pkg = placed.get(candidate);
|
|
601
|
+
if (!pkg)
|
|
602
|
+
continue; // never asked or failed: the walk goes on upward
|
|
603
|
+
if (!isSemverRange(req.range) || satisfiesRange(pkg.version, req.range)) {
|
|
604
|
+
decided = true; // reuse
|
|
605
|
+
}
|
|
606
|
+
else if (edge.kind === 'peer') {
|
|
607
|
+
decided = true; // reuse anyway, and say so the way npm does
|
|
608
|
+
log(` [warn] peer ${edge.name}@${edge.range} from ${edge.from} is met by ${pkg.name}@${pkg.version} (${candidate})`);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
target = nestedPlacement(edge.from, edge.name);
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
if (decided)
|
|
616
|
+
continue;
|
|
617
|
+
if (settled.has(target))
|
|
618
|
+
continue; // failed once, on record under the name
|
|
619
|
+
if (target.split('/node_modules/').length - 1 > MAX_NEST_DEPTH) {
|
|
620
|
+
unresolved.set(edge.name, `${edge.name}@${edge.range} from ${edge.from} would nest deeper than ${MAX_NEST_DEPTH} levels (cycle with incompatible ranges)`);
|
|
621
|
+
settled.add(target);
|
|
554
622
|
continue;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
layer.push(
|
|
623
|
+
}
|
|
624
|
+
pending.add(target);
|
|
625
|
+
layer.push({ edge, placement: target });
|
|
558
626
|
}
|
|
559
|
-
queue =
|
|
627
|
+
queue = deferred;
|
|
560
628
|
if (__f2Diag) {
|
|
561
|
-
log(`[f2-frontier] N=${layerN} width=${layer.length} resolved-so-far=${resolved.size}
|
|
629
|
+
log(`[f2-frontier] N=${layerN} width=${layer.length} resolved-so-far=${resolved.size} settled=${settled.size}`);
|
|
562
630
|
}
|
|
563
631
|
if (layer.length === 0)
|
|
564
632
|
break;
|
|
@@ -570,7 +638,7 @@ export class NpmInstaller {
|
|
|
570
638
|
// cache slice (only entries for THIS name) so the per-task RPC
|
|
571
639
|
// stays small. Bounded to 16 versions per name — enough to cover
|
|
572
640
|
// ~2 majors of typical packages, well under any RPC arg size cap.
|
|
573
|
-
const tasks = layer.map((
|
|
641
|
+
const tasks = layer.map(({ edge: { name, range }, placement }) => {
|
|
574
642
|
const cachedRows = this.cache.getRegistryVersions(name).slice(0, 16);
|
|
575
643
|
const cachedEntries = cachedRows.map((e) => ({
|
|
576
644
|
name: e.name,
|
|
@@ -594,8 +662,9 @@ export class NpmInstaller {
|
|
|
594
662
|
isOptional: optionalNames.has(name),
|
|
595
663
|
fetchTimeoutMs: 15_000,
|
|
596
664
|
retries: 3,
|
|
665
|
+
registry,
|
|
597
666
|
};
|
|
598
|
-
return { key:
|
|
667
|
+
return { key: placement, args: taskSpec };
|
|
599
668
|
});
|
|
600
669
|
// Dispatch the layer. Fanout routes:
|
|
601
670
|
// <5 → in-DO fanout in-DO (IsolatePool), concurrency = layer.length (capped at 4)
|
|
@@ -617,7 +686,10 @@ export class NpmInstaller {
|
|
|
617
686
|
// fanout failed to deliver is recorded instead of skipped.
|
|
618
687
|
for (let i = 0; i < layer.length; i++) {
|
|
619
688
|
const res = results[i];
|
|
620
|
-
const
|
|
689
|
+
const { edge, placement } = layer[i];
|
|
690
|
+
const taskName = edge.name;
|
|
691
|
+
pending.delete(placement);
|
|
692
|
+
settled.add(placement);
|
|
621
693
|
if (!res) {
|
|
622
694
|
unresolved.set(taskName, `resolver fanout returned no result at layer ${layerN}`);
|
|
623
695
|
continue;
|
|
@@ -643,10 +715,10 @@ export class NpmInstaller {
|
|
|
643
715
|
// One `npm http` line per packument, reporting the tier that actually
|
|
644
716
|
// served it. A skipped task issued no request, so it gets no line.
|
|
645
717
|
if (res.packumentSource === 'network') {
|
|
646
|
-
this.npmLog('http', npmHttpFetchLine(packumentUrl(taskName), res.packumentElapsedMs));
|
|
718
|
+
this.npmLog('http', npmHttpFetchLine(packumentUrl(taskName, registry), res.packumentElapsedMs));
|
|
647
719
|
}
|
|
648
720
|
else if (res.packumentSource !== 'skipped') {
|
|
649
|
-
this.npmLog('http', npmHttpCacheLine(packumentUrl(taskName)));
|
|
721
|
+
this.npmLog('http', npmHttpCacheLine(packumentUrl(taskName, registry)));
|
|
650
722
|
}
|
|
651
723
|
totalPackumentBytes += res.packumentBytesDecoded;
|
|
652
724
|
if (res.packumentSource === 'r2-cache')
|
|
@@ -716,37 +788,39 @@ export class NpmInstaller {
|
|
|
716
788
|
continue;
|
|
717
789
|
}
|
|
718
790
|
}
|
|
719
|
-
|
|
791
|
+
// A W6 swap answers with another name; place what came back.
|
|
792
|
+
const isRoot = placement === taskName;
|
|
793
|
+
const actual = isRoot ? pkg.name : nestedPlacement(edge.from, pkg.name);
|
|
794
|
+
if (placed.has(actual))
|
|
720
795
|
continue;
|
|
721
|
-
|
|
722
|
-
|
|
796
|
+
placed.set(actual, pkg);
|
|
797
|
+
settled.add(actual);
|
|
798
|
+
if (isRoot)
|
|
799
|
+
resolved.set(pkg.name, pkg);
|
|
800
|
+
else
|
|
801
|
+
nested.set(actual, pkg);
|
|
802
|
+
// Edge extraction, from this package's own placement.
|
|
723
803
|
const inheritBestEffort = bestEffortNames.has(pkg.name);
|
|
724
804
|
for (const [depName, depRange] of Object.entries(pkg.dependencies)) {
|
|
725
|
-
if (resolved.has(depName) || seen.has(depName))
|
|
726
|
-
continue;
|
|
727
805
|
if (inheritBestEffort)
|
|
728
806
|
bestEffortNames.add(depName);
|
|
729
|
-
queue.push(
|
|
807
|
+
queue.push({ name: depName, range: depRange, from: actual, kind: 'dep' });
|
|
730
808
|
}
|
|
731
809
|
const optDeps = pkg.optionalDependencies;
|
|
732
810
|
if (optDeps) {
|
|
733
811
|
for (const [depName, depRange] of Object.entries(optDeps)) {
|
|
734
|
-
if (resolved.has(depName) || seen.has(depName))
|
|
735
|
-
continue;
|
|
736
812
|
optionalNames.add(depName);
|
|
737
813
|
if (inheritBestEffort)
|
|
738
814
|
bestEffortNames.add(depName);
|
|
739
|
-
queue.push(
|
|
815
|
+
queue.push({ name: depName, range: depRange, from: actual, kind: 'dep' });
|
|
740
816
|
}
|
|
741
817
|
}
|
|
742
818
|
if (pkg.peerDependencies) {
|
|
743
819
|
for (const [peerName, peerRange] of Object.entries(pkg.peerDependencies)) {
|
|
744
|
-
if (resolved.has(peerName) || seen.has(peerName))
|
|
745
|
-
continue;
|
|
746
820
|
topLevelNames.add(peerName);
|
|
747
821
|
if (inheritBestEffort)
|
|
748
822
|
bestEffortNames.add(peerName);
|
|
749
|
-
queue.push(
|
|
823
|
+
queue.push({ name: peerName, range: peerRange, from: actual, kind: 'peer' });
|
|
750
824
|
}
|
|
751
825
|
}
|
|
752
826
|
// X.5-F R2.5 + X.5-J: optional peers when THIS pkg is the
|
|
@@ -757,11 +831,9 @@ export class NpmInstaller {
|
|
|
757
831
|
const allPeers = pkg.__allPeerDependencies;
|
|
758
832
|
if (allPeers) {
|
|
759
833
|
for (const [peerName, peerRange] of Object.entries(allPeers)) {
|
|
760
|
-
if (resolved.has(peerName) || seen.has(peerName))
|
|
761
|
-
continue;
|
|
762
834
|
topLevelNames.add(peerName);
|
|
763
835
|
bestEffortNames.add(peerName);
|
|
764
|
-
queue.push(
|
|
836
|
+
queue.push({ name: peerName, range: peerRange, from: actual, kind: 'peer' });
|
|
765
837
|
}
|
|
766
838
|
}
|
|
767
839
|
}
|
|
@@ -776,13 +848,18 @@ export class NpmInstaller {
|
|
|
776
848
|
// sees names in BFS order, so incremental propagation would freeze a
|
|
777
849
|
// dev/optional first sighting before a later required edge lands.
|
|
778
850
|
const requiredNames = new Set();
|
|
851
|
+
// Followed by name over every placement, root first.
|
|
852
|
+
const byName = new Map(resolved);
|
|
853
|
+
for (const pkg of nested.values())
|
|
854
|
+
if (!byName.has(pkg.name))
|
|
855
|
+
byName.set(pkg.name, pkg);
|
|
779
856
|
const pendingRequired = Object.keys(specs).filter((name) => !optionalRoots.has(name));
|
|
780
857
|
for (let i = 0; i < pendingRequired.length; i++) {
|
|
781
858
|
const name = pendingRequired[i];
|
|
782
859
|
if (requiredNames.has(name))
|
|
783
860
|
continue;
|
|
784
861
|
requiredNames.add(name);
|
|
785
|
-
const pkg =
|
|
862
|
+
const pkg = byName.get(name);
|
|
786
863
|
if (!pkg)
|
|
787
864
|
continue;
|
|
788
865
|
const optional = pkg.optionalDependencies ?? {};
|
|
@@ -829,7 +906,7 @@ export class NpmInstaller {
|
|
|
829
906
|
`, dispatch barriers=${dispatchBarriers}, ` +
|
|
830
907
|
`elapsed=${((Date.now() - t0) / 1000).toFixed(1)}s` +
|
|
831
908
|
(unresolved.size > 0 ? `, unresolved=${unresolved.size}` : ''));
|
|
832
|
-
return { resolved, unresolved, rejected };
|
|
909
|
+
return { resolved, nested, unresolved, rejected };
|
|
833
910
|
}
|
|
834
911
|
/**
|
|
835
912
|
* Batch install via two-tier fan-out (Fanout).
|
|
@@ -855,31 +932,26 @@ export class NpmInstaller {
|
|
|
855
932
|
* Two-tier topology re-expands the fan-out without re-introducing
|
|
856
933
|
* the V8 cap risk.
|
|
857
934
|
*/
|
|
858
|
-
async fetchViaBatchFacet(toFetch,
|
|
935
|
+
async fetchViaBatchFacet(toFetch, nmDir, pid) {
|
|
859
936
|
const log = (msg) => this.onProgress?.(msg);
|
|
860
937
|
const installed = [];
|
|
861
938
|
const failed = [];
|
|
862
939
|
let filesWritten = 0;
|
|
863
940
|
const mtime = Date.now();
|
|
864
941
|
// Every entry in `toFetch` has a tarball URL — the diff phase fails
|
|
865
|
-
// the ones that don't rather than dropping them.
|
|
942
|
+
// the ones that don't rather than dropping them. `pkgDir` is the
|
|
943
|
+
// spec's identity: one version may land at two placements.
|
|
866
944
|
const specs = toFetch
|
|
867
|
-
.map((p) => ({
|
|
945
|
+
.map(({ placement, pkg: p }) => ({
|
|
868
946
|
name: p.name,
|
|
869
947
|
version: p.version,
|
|
870
948
|
tarballUrl: p.tarballUrl,
|
|
871
949
|
integrity: p.integrity || '',
|
|
872
|
-
pkgDir: nmDir + '/' +
|
|
950
|
+
pkgDir: nmDir + '/' + placement,
|
|
873
951
|
installRoot: nmDir,
|
|
874
952
|
mtime,
|
|
875
953
|
chunkSize: CHUNK_SIZE,
|
|
876
954
|
}));
|
|
877
|
-
// hoistPlan is intentionally unused: the current installer maps
|
|
878
|
-
// every package to `${nmDir}/${name}` (flat hoisting). Accepting
|
|
879
|
-
// the plan as a parameter keeps the caller agnostic of the hoist
|
|
880
|
-
// strategy and lets a future nested-install variant slot in
|
|
881
|
-
// without changing the call site.
|
|
882
|
-
void hoistPlan;
|
|
883
955
|
if (specs.length === 0) {
|
|
884
956
|
return { installed, failed, filesWritten };
|
|
885
957
|
}
|
|
@@ -899,13 +971,22 @@ export class NpmInstaller {
|
|
|
899
971
|
// the two made the phase width control install parallelism, so widening it
|
|
900
972
|
// for latency widened the cold-start burst along with it.
|
|
901
973
|
const INSTALL_PEER_CAP = 8;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
974
|
+
// A tarball's placements share a shard, which acquires it once.
|
|
975
|
+
const byTarball = new Map();
|
|
976
|
+
for (const spec of specs) {
|
|
977
|
+
const group = byTarball.get(spec.tarballUrl);
|
|
978
|
+
if (group)
|
|
979
|
+
group.push(spec);
|
|
980
|
+
else
|
|
981
|
+
byTarball.set(spec.tarballUrl, [spec]);
|
|
982
|
+
}
|
|
983
|
+
const shardCount = Math.min(byTarball.size, INSTALL_PEER_CAP);
|
|
984
|
+
// Round-robin assignment: tarball at idx → shard idx % shardCount.
|
|
985
|
+
// This produces ⌈tarballs / shardCount⌉ tarballs per shard at
|
|
905
986
|
// most, with the imbalance bounded to ±1.
|
|
906
987
|
const shards = Array.from({ length: shardCount }, () => []);
|
|
907
|
-
|
|
908
|
-
shards[idx % shardCount].push(
|
|
988
|
+
[...byTarball.values()].forEach((group, idx) => {
|
|
989
|
+
shards[idx % shardCount].push(...group);
|
|
909
990
|
});
|
|
910
991
|
const nonEmptyShards = shards.filter((s) => s.length > 0);
|
|
911
992
|
const topology = nonEmptyShards.length < IN_DO_THRESHOLD ? 'in-do (in-DO fanout)' : 'peer-do (peer-DO fanout)';
|
|
@@ -967,11 +1048,13 @@ export class NpmInstaller {
|
|
|
967
1048
|
let okCount = 0;
|
|
968
1049
|
let failCount = 0;
|
|
969
1050
|
const reported = new Set();
|
|
970
|
-
const
|
|
1051
|
+
const specByDir = new Map(specs.map((s) => [s.pkgDir, s]));
|
|
971
1052
|
for (const r of result.perPackage) {
|
|
972
|
-
|
|
1053
|
+
// A pre-placement shard reports no pkgDir; it only wrote root.
|
|
1054
|
+
const dir = r.pkgDir ?? `${nmDir}/${r.name}`;
|
|
1055
|
+
reported.add(dir);
|
|
973
1056
|
// One `npm http` line per tarball, reporting the tier that served it.
|
|
974
|
-
const spec =
|
|
1057
|
+
const spec = specByDir.get(dir);
|
|
975
1058
|
if (spec && r.tarballSource) {
|
|
976
1059
|
this.npmLog('http', r.tarballSource === 'registry'
|
|
977
1060
|
? npmHttpFetchLine(spec.tarballUrl, r.tarballElapsedMs ?? 0)
|
|
@@ -996,12 +1079,11 @@ export class NpmInstaller {
|
|
|
996
1079
|
// returns short would otherwise leave its packages in neither list,
|
|
997
1080
|
// which is the same silent partial one layer down.
|
|
998
1081
|
for (const s of specs) {
|
|
999
|
-
|
|
1000
|
-
if (reported.has(id))
|
|
1082
|
+
if (reported.has(s.pkgDir))
|
|
1001
1083
|
continue;
|
|
1002
|
-
failed.push(
|
|
1084
|
+
failed.push(`${s.name}@${s.version}`);
|
|
1003
1085
|
failCount++;
|
|
1004
|
-
log(` [warn] ${
|
|
1086
|
+
log(` [warn] ${s.name}@${s.version}: install shard returned no result for this package (${s.pkgDir})`);
|
|
1005
1087
|
}
|
|
1006
1088
|
// Fold facet counters into the supervisor's diagnostic state.
|
|
1007
1089
|
recordInstallFacetCounters(result.facetCounters);
|
|
@@ -1101,6 +1183,13 @@ export class NpmInstaller {
|
|
|
1101
1183
|
* W6: apply the PACKAGE_ABI_POLICY swap rewrites and reject deny list
|
|
1102
1184
|
* to a top-level spec map. Emits `[swap]` notices via onProgress.
|
|
1103
1185
|
*
|
|
1186
|
+
* A swap rewrites the spec's RANGE to an npm alias of the swap target
|
|
1187
|
+
* (`esbuild` → `npm:esbuild-wasm@<range>`), never its key: the key names
|
|
1188
|
+
* the install directory, so `resolved`, node_modules, the bin links, the
|
|
1189
|
+
* lockfile and package.json all keep the name the user declared. The
|
|
1190
|
+
* resolver facet treats the alias as authoritative and does not announce
|
|
1191
|
+
* the swap a second time.
|
|
1192
|
+
*
|
|
1104
1193
|
* G2: rejects are advisories — a listed package stays in the spec map
|
|
1105
1194
|
* and installs like any other (npm parity: it cannot run here, but
|
|
1106
1195
|
* install is the wrong place to say so). Each gets one `[npm] note:`
|
|
@@ -1166,7 +1255,8 @@ export class NpmInstaller {
|
|
|
1166
1255
|
// info comes from the registry cache — if the cache miss happens
|
|
1167
1256
|
// too, we play it safe and invalidate (a fresh resolve repopulates
|
|
1168
1257
|
// it and is always correct).
|
|
1169
|
-
|
|
1258
|
+
// An edge is met by any placement Node's walk from the entry finds.
|
|
1259
|
+
for (const [placement, entry] of lockfile) {
|
|
1170
1260
|
const cached = this.cache.getRegistryEntry(entry.name, entry.resolvedVer);
|
|
1171
1261
|
if (!cached)
|
|
1172
1262
|
return false;
|
|
@@ -1175,67 +1265,185 @@ export class NpmInstaller {
|
|
|
1175
1265
|
for (const depName of Object.keys(deps)) {
|
|
1176
1266
|
if (Object.hasOwn(optional, depName))
|
|
1177
1267
|
continue;
|
|
1178
|
-
if (!lockfile.has(
|
|
1268
|
+
if (!visiblePlacements(placement, depName).some((p) => lockfile.has(p)))
|
|
1179
1269
|
return false;
|
|
1180
1270
|
}
|
|
1181
1271
|
const peers = safeJsonParse(cached.peerDepsJson || '{}', {});
|
|
1182
1272
|
for (const peerName of Object.keys(peers)) {
|
|
1183
|
-
if (!lockfile.has(
|
|
1273
|
+
if (!visiblePlacements(placement, peerName).some((p) => lockfile.has(p)))
|
|
1184
1274
|
return false;
|
|
1185
1275
|
}
|
|
1186
1276
|
}
|
|
1187
1277
|
return true;
|
|
1188
1278
|
}
|
|
1189
1279
|
/**
|
|
1190
|
-
*
|
|
1280
|
+
* npm ci: the placements package-lock.json (or npm-shrinkwrap.json)
|
|
1281
|
+
* records, checked against package.json first. The ABI policy the resolver
|
|
1282
|
+
* applies still holds: platform-native optional shards are skipped, a
|
|
1283
|
+
* required one is refused, and a swapped package resolves its swap target
|
|
1284
|
+
* at the locked version.
|
|
1191
1285
|
*/
|
|
1192
|
-
|
|
1286
|
+
async treeFromPackageLock(projDir, production, log, registry) {
|
|
1287
|
+
const lockName = ['npm-shrinkwrap.json', 'package-lock.json']
|
|
1288
|
+
.find((name) => this.vfs.exists(`${projDir}/${name}`));
|
|
1289
|
+
if (!lockName)
|
|
1290
|
+
throw new Error('`npm ci` needs a package-lock.json or npm-shrinkwrap.json');
|
|
1291
|
+
const lock = parsePackageLock(this.vfs.readFileString(`${projDir}/${lockName}`), lockName);
|
|
1292
|
+
const pkgJsonPath = `${projDir}/package.json`;
|
|
1293
|
+
if (!this.vfs.exists(pkgJsonPath))
|
|
1294
|
+
throw new Error('`npm ci` needs a package.json');
|
|
1295
|
+
const pkgJson = JSON.parse(this.vfs.readFileString(pkgJsonPath));
|
|
1296
|
+
const mismatches = packageLockMismatches(pkgJson, lock);
|
|
1297
|
+
if (mismatches.length > 0) {
|
|
1298
|
+
throw new Error(`\`npm ci\` can only install packages when your package.json and ${lockName} are in sync. ` +
|
|
1299
|
+
`Update the lock file with \`npm install\` before continuing.\n` +
|
|
1300
|
+
mismatches.map((m) => `npm ERR! ${m}`).join('\n'));
|
|
1301
|
+
}
|
|
1302
|
+
const unsupported = Object.keys(lock.packages).find((key) => key !== '' && !key.startsWith('node_modules/'));
|
|
1303
|
+
if (unsupported !== undefined) {
|
|
1304
|
+
throw new Error(`${lockName} entry "${unsupported}" is a workspace or linked package, which \`npm ci\` here does not install`);
|
|
1305
|
+
}
|
|
1306
|
+
// Only a lock that will be installed as written clears the old tree.
|
|
1307
|
+
if (this.vfs.exists(`${projDir}/node_modules`))
|
|
1308
|
+
this.vfs.removeRecursive(`${projDir}/node_modules`);
|
|
1193
1309
|
const resolved = new Map();
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1310
|
+
const nested = new Map();
|
|
1311
|
+
const unresolved = new Map();
|
|
1312
|
+
const rejected = [];
|
|
1313
|
+
// Swapped lock entries by placement: each resolves its swap target at its own locked version.
|
|
1314
|
+
const swapPlacements = [];
|
|
1315
|
+
for (const [key, entry] of Object.entries(lock.packages)) {
|
|
1316
|
+
if (key === '')
|
|
1317
|
+
continue;
|
|
1318
|
+
if (production && entry.dev === true)
|
|
1319
|
+
continue;
|
|
1320
|
+
const placement = key.slice('node_modules/'.length);
|
|
1321
|
+
const folderName = placement.split('/node_modules/').pop() ?? placement;
|
|
1322
|
+
const name = typeof entry.name === 'string' ? entry.name : folderName;
|
|
1323
|
+
const version = typeof entry.version === 'string' ? entry.version : '';
|
|
1324
|
+
const platform = {
|
|
1325
|
+
name,
|
|
1326
|
+
os: stringList(entry.os),
|
|
1327
|
+
cpu: stringList(entry.cpu),
|
|
1328
|
+
libc: stringList(entry.libc),
|
|
1329
|
+
main: '',
|
|
1330
|
+
};
|
|
1331
|
+
if (entry.optional === true && isOptionalNativeBinding(platform)) {
|
|
1332
|
+
log(`[resolve-fanout] [skip] ${name} — optional native binding (os=${platform.os ?? '*'}, cpu=${platform.cpu ?? '*'}, libc=${platform.libc ?? '*'})`);
|
|
1333
|
+
continue;
|
|
1209
1334
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1335
|
+
const platformReject = policyNativePlatformReject(PACKAGE_ABI_POLICY, platform);
|
|
1336
|
+
if (platformReject) {
|
|
1337
|
+
log(`[resolve-fanout] [skip] ${name} — ${platformReject.reason}`);
|
|
1338
|
+
rejected.push({ name, reason: platformReject.reason, required: entry.optional !== true });
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
// Policy follows the registry identity (`name`), placement the folder the lock
|
|
1342
|
+
// names: an aliased entry (`node_modules/build`, name `esbuild`) resolves as
|
|
1343
|
+
// the alias `build@npm:esbuild@<locked>`, so the swap target lands in build.
|
|
1344
|
+
if (lookupSwap(name)) {
|
|
1345
|
+
swapPlacements.push({ placement, folderName, spec: folderName === name ? version : `npm:${name}@${version}` });
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
if (entry.link === true || typeof entry.resolved !== 'string' || !version) {
|
|
1349
|
+
unresolved.set(name, `${lockName} records no registry tarball for ${key}`);
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
const reject = lookupReject(name);
|
|
1353
|
+
if (reject)
|
|
1354
|
+
log(`[npm] note: ${name} has no Workers-compatible build: ${reject.reason}${reject.suggest ? ` … try: ${reject.suggest}` : ''}`);
|
|
1355
|
+
const pkg = {
|
|
1356
|
+
name,
|
|
1357
|
+
version,
|
|
1358
|
+
tarballUrl: entry.resolved,
|
|
1359
|
+
integrity: typeof entry.integrity === 'string' ? entry.integrity : '',
|
|
1360
|
+
dependencies: stringRecord(entry.dependencies),
|
|
1361
|
+
optionalDependencies: stringRecord(entry.optionalDependencies),
|
|
1362
|
+
peerDependencies: stringRecord(entry.peerDependencies),
|
|
1363
|
+
os: platform.os,
|
|
1364
|
+
cpu: platform.cpu,
|
|
1365
|
+
libc: platform.libc,
|
|
1366
|
+
exports: null,
|
|
1367
|
+
main: '',
|
|
1368
|
+
module: '',
|
|
1369
|
+
bin: typeof entry.bin === 'string' ? { [folderName]: entry.bin } : stringRecord(entry.bin),
|
|
1370
|
+
};
|
|
1371
|
+
const staged = lookupStagedArtifact(name);
|
|
1372
|
+
if (staged)
|
|
1373
|
+
applyStagedArtifact(pkg, staged);
|
|
1374
|
+
(placement.includes('/node_modules/') ? nested : resolved).set(placement, pkg);
|
|
1375
|
+
}
|
|
1376
|
+
// Root placements resolve together, as top-level specs. A nested one is its own
|
|
1377
|
+
// resolution, rebased under its placement: its package there, anything it
|
|
1378
|
+
// brings below it, unless the lock already placed that path.
|
|
1379
|
+
const rootSwaps = swapPlacements.filter(({ placement }) => !placement.includes('/node_modules/'));
|
|
1380
|
+
const batches = [];
|
|
1381
|
+
if (rootSwaps.length > 0)
|
|
1382
|
+
batches.push({ at: null, specs: Object.fromEntries(rootSwaps.map(({ folderName, spec }) => [folderName, spec])) });
|
|
1383
|
+
for (const { placement, folderName, spec } of swapPlacements) {
|
|
1384
|
+
if (placement.includes('/node_modules/'))
|
|
1385
|
+
batches.push({ at: placement, specs: { [folderName]: spec } });
|
|
1386
|
+
}
|
|
1387
|
+
for (const { at, specs } of batches) {
|
|
1388
|
+
const swapped = await this.resolveTreeViaFanout(specs, log, { registry });
|
|
1389
|
+
const [folder] = Object.keys(specs);
|
|
1390
|
+
const rebase = (path) => (at === null ? path : path === folder ? at : `${at}/node_modules/${path}`);
|
|
1391
|
+
for (const [path, pkg] of [...swapped.resolved, ...swapped.nested]) {
|
|
1392
|
+
const target = rebase(path);
|
|
1393
|
+
const into = target.includes('/node_modules/') ? nested : resolved;
|
|
1394
|
+
if (!into.has(target))
|
|
1395
|
+
into.set(target, pkg);
|
|
1223
1396
|
}
|
|
1397
|
+
for (const [name, reason] of swapped.unresolved)
|
|
1398
|
+
unresolved.set(name, reason);
|
|
1399
|
+
rejected.push(...swapped.rejected);
|
|
1224
1400
|
}
|
|
1225
|
-
return resolved;
|
|
1401
|
+
return { resolved, nested, unresolved, rejected, lockName };
|
|
1402
|
+
}
|
|
1403
|
+
/** Convert a lockfile back to resolved packages: root by name, nested by placement path. */
|
|
1404
|
+
lockfileToResolved(lockfile) {
|
|
1405
|
+
const resolved = new Map();
|
|
1406
|
+
const nested = new Map();
|
|
1407
|
+
for (const [placement, entry] of lockfile) {
|
|
1408
|
+
const name = entry.name;
|
|
1409
|
+
// Reconstruct from the registry cache; on a miss, a minimal entry.
|
|
1410
|
+
const cached = this.cache.getRegistryEntry(name, entry.resolvedVer);
|
|
1411
|
+
(placement === name ? resolved : nested).set(placement, cached ? {
|
|
1412
|
+
name: cached.name,
|
|
1413
|
+
version: cached.version,
|
|
1414
|
+
tarballUrl: cached.tarballUrl,
|
|
1415
|
+
integrity: cached.integrity,
|
|
1416
|
+
dependencies: safeJsonParse(cached.depsJson, {}),
|
|
1417
|
+
exports: safeJsonParse(cached.exportsJson, null),
|
|
1418
|
+
main: cached.main,
|
|
1419
|
+
module: cached.moduleField,
|
|
1420
|
+
bin: safeJsonParse(cached.binJson, {}),
|
|
1421
|
+
} : {
|
|
1422
|
+
name,
|
|
1423
|
+
version: entry.resolvedVer,
|
|
1424
|
+
tarballUrl: '',
|
|
1425
|
+
integrity: entry.integrity,
|
|
1426
|
+
dependencies: safeJsonParse(entry.depsJson, {}),
|
|
1427
|
+
exports: null,
|
|
1428
|
+
main: '',
|
|
1429
|
+
module: '',
|
|
1430
|
+
bin: {},
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
return { resolved, nested };
|
|
1226
1434
|
}
|
|
1227
1435
|
/**
|
|
1228
1436
|
* Write lockfile to SQLite.
|
|
1229
1437
|
*/
|
|
1230
|
-
writeLockfile(projDir,
|
|
1438
|
+
writeLockfile(projDir, hoistPlan, nmDir) {
|
|
1231
1439
|
const entries = new Map();
|
|
1232
|
-
for (const
|
|
1233
|
-
entries.set(
|
|
1234
|
-
name,
|
|
1440
|
+
for (const { placement, pkg } of hoistPlacements(hoistPlan)) {
|
|
1441
|
+
entries.set(placement, {
|
|
1442
|
+
name: pkg.name,
|
|
1235
1443
|
resolvedVer: pkg.version,
|
|
1236
1444
|
integrity: pkg.integrity,
|
|
1237
1445
|
depsJson: JSON.stringify(pkg.dependencies),
|
|
1238
|
-
hoistedPath: nmDir + '/' +
|
|
1446
|
+
hoistedPath: nmDir + '/' + placement,
|
|
1239
1447
|
});
|
|
1240
1448
|
}
|
|
1241
1449
|
this.cache.writeLockfile(projDir, entries, this.ctx);
|
|
@@ -1259,7 +1467,7 @@ export class NpmInstaller {
|
|
|
1259
1467
|
const binChunks = [];
|
|
1260
1468
|
const mtime = Date.now();
|
|
1261
1469
|
for (const binEntry of Object.values(manifest.bins)) {
|
|
1262
|
-
const data = enc.encode(createNpmBinShim(binEntry));
|
|
1470
|
+
const data = enc.encode(createNpmBinShim(binEntry, binDir));
|
|
1263
1471
|
const linkPath = binDir + '/' + binEntry.name;
|
|
1264
1472
|
binEntries.push({
|
|
1265
1473
|
path: linkPath,
|
|
@@ -1313,21 +1521,12 @@ export class NpmInstaller {
|
|
|
1313
1521
|
if (!pkgJson.dependencies)
|
|
1314
1522
|
pkgJson.dependencies = {};
|
|
1315
1523
|
for (const spec of explicitPackages) {
|
|
1316
|
-
//
|
|
1524
|
+
// `resolved` is keyed by the name the user typed — a swapped
|
|
1525
|
+
// package resolves as an alias under that name (applySwaps), so
|
|
1526
|
+
// `esbuild` is found as `esbuild` and package.json records the
|
|
1527
|
+
// user's key, never the swap target.
|
|
1317
1528
|
const { name } = parseExplicitPackageSpec(spec);
|
|
1318
|
-
|
|
1319
|
-
// keyed by the swap target (e.g. user typed 'esbuild', resolved
|
|
1320
|
-
// has 'esbuild-wasm'). Look up via lookupSwap to bridge the
|
|
1321
|
-
// gap; write the user's original key into package.json so the
|
|
1322
|
-
// file remains the user's source-of-truth and isn't silently
|
|
1323
|
-
// mutated to the swap target (which would break cross-environment
|
|
1324
|
-
// pushes).
|
|
1325
|
-
let pkg = resolved.get(name);
|
|
1326
|
-
if (!pkg) {
|
|
1327
|
-
const swap = lookupSwap(name);
|
|
1328
|
-
if (swap)
|
|
1329
|
-
pkg = resolved.get(swap.to);
|
|
1330
|
-
}
|
|
1529
|
+
const pkg = resolved.get(name);
|
|
1331
1530
|
if (pkg) {
|
|
1332
1531
|
pkgJson.dependencies[name] = '^' + pkg.version;
|
|
1333
1532
|
}
|