@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.
Files changed (286) hide show
  1. package/NOTICE.md +5 -0
  2. package/README.md +10 -0
  3. package/dist/_shared/session-router.d.ts +7 -0
  4. package/dist/_shared/session-router.d.ts.map +1 -1
  5. package/dist/_shared/session-router.js +7 -0
  6. package/dist/bindings/body.d.ts +6 -0
  7. package/dist/bindings/body.d.ts.map +1 -0
  8. package/dist/bindings/body.js +37 -0
  9. package/dist/bindings/kv.d.ts +0 -2
  10. package/dist/bindings/kv.d.ts.map +1 -1
  11. package/dist/bindings/kv.js +3 -42
  12. package/dist/bindings/r2.d.ts +0 -2
  13. package/dist/bindings/r2.d.ts.map +1 -1
  14. package/dist/bindings/r2.js +4 -44
  15. package/dist/esbuild-cli-artifact.generated.d.ts +18 -0
  16. package/dist/esbuild-cli-artifact.generated.d.ts.map +1 -0
  17. package/dist/esbuild-cli-artifact.generated.js +17 -0
  18. package/dist/esbuild-wasm-bundle.generated.d.ts +24 -24
  19. package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -1
  20. package/dist/esbuild-wasm-bundle.generated.js +24 -24
  21. package/dist/facets/cirrus-real.d.ts.map +1 -1
  22. package/dist/facets/cirrus-real.js +11 -6
  23. package/dist/facets/compose.d.ts +11 -7
  24. package/dist/facets/compose.d.ts.map +1 -1
  25. package/dist/facets/compose.js +8 -31
  26. package/dist/facets/data-plan.d.ts +79 -0
  27. package/dist/facets/data-plan.d.ts.map +1 -0
  28. package/dist/facets/data-plan.js +477 -0
  29. package/dist/facets/esbuild-bundle-pool.d.ts +1 -1
  30. package/dist/facets/esbuild-bundle-pool.js +3 -3
  31. package/dist/facets/esbuild-transform.d.ts +38 -8
  32. package/dist/facets/esbuild-transform.d.ts.map +1 -1
  33. package/dist/facets/esbuild-transform.js +219 -19
  34. package/dist/facets/exec-telemetry.d.ts +7 -3
  35. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  36. package/dist/facets/manager.d.ts +198 -69
  37. package/dist/facets/manager.d.ts.map +1 -1
  38. package/dist/facets/manager.js +1276 -652
  39. package/dist/facets/opencode-staging.d.ts +0 -2
  40. package/dist/facets/opencode-staging.d.ts.map +1 -1
  41. package/dist/facets/opencode-staging.js +5 -11
  42. package/dist/facets/process.d.ts +4 -0
  43. package/dist/facets/process.d.ts.map +1 -1
  44. package/dist/facets/process.js +3 -0
  45. package/dist/facets/read-profile.d.ts +149 -0
  46. package/dist/facets/read-profile.d.ts.map +1 -0
  47. package/dist/facets/read-profile.js +483 -0
  48. package/dist/facets/real-vite-hmr.d.ts +2 -0
  49. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  50. package/dist/facets/real-vite-hmr.js +3 -2
  51. package/dist/facets/resident-identity.d.ts +0 -1
  52. package/dist/facets/resident-identity.d.ts.map +1 -1
  53. package/dist/facets/resident-identity.js +0 -3
  54. package/dist/facets/wasm-swap-registry.d.ts +34 -25
  55. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  56. package/dist/facets/wasm-swap-registry.js +62 -56
  57. package/dist/facets/ws-terminal.d.ts +4 -4
  58. package/dist/facets/ws-terminal.d.ts.map +1 -1
  59. package/dist/facets/ws-terminal.js +4 -5
  60. package/dist/git/commands.d.ts +33 -10
  61. package/dist/git/commands.d.ts.map +1 -1
  62. package/dist/git/commands.js +1891 -197
  63. package/dist/git/network-facet.d.ts +12 -2
  64. package/dist/git/network-facet.d.ts.map +1 -1
  65. package/dist/git/network-facet.js +69 -18
  66. package/dist/git/unified-diff.d.ts +74 -0
  67. package/dist/git/unified-diff.d.ts.map +1 -0
  68. package/dist/git/unified-diff.js +676 -0
  69. package/dist/git-bundle.generated.d.ts +7 -11
  70. package/dist/git-bundle.generated.d.ts.map +1 -1
  71. package/dist/git-bundle.generated.js +7 -11
  72. package/dist/hosted/commands.d.ts.map +1 -1
  73. package/dist/hosted/commands.js +64 -245
  74. package/dist/hosted/runtime.d.ts +22 -15
  75. package/dist/hosted/runtime.d.ts.map +1 -1
  76. package/dist/hosted/runtime.js +72 -14
  77. package/dist/hosted/services.d.ts +8 -1
  78. package/dist/hosted/services.d.ts.map +1 -1
  79. package/dist/hosted/services.js +12 -13
  80. package/dist/hosted/session.d.ts +165 -0
  81. package/dist/hosted/session.d.ts.map +1 -0
  82. package/dist/hosted/session.js +132 -0
  83. package/dist/loaders/generated-workers.d.ts +6 -3
  84. package/dist/loaders/generated-workers.d.ts.map +1 -1
  85. package/dist/loaders/generated-workers.js +8 -5
  86. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  87. package/dist/loaders/npm-resolve-preamble.js +0 -27
  88. package/dist/loaders/pre-bundle-preamble.d.ts +6 -3
  89. package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
  90. package/dist/loaders/pre-bundle-preamble.js +13 -8
  91. package/dist/node-shims-artifact.generated.d.ts +13 -4
  92. package/dist/node-shims-artifact.generated.d.ts.map +1 -1
  93. package/dist/node-shims-artifact.generated.js +16 -7
  94. package/dist/npm/bin-links.d.ts +3 -1
  95. package/dist/npm/bin-links.d.ts.map +1 -1
  96. package/dist/npm/bin-links.js +32 -8
  97. package/dist/npm/cache.d.ts +11 -1
  98. package/dist/npm/cache.d.ts.map +1 -1
  99. package/dist/npm/cache.js +21 -7
  100. package/dist/npm/install-batch-facet.d.ts +4 -2
  101. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  102. package/dist/npm/install-batch-facet.js +32 -18
  103. package/dist/npm/installer.d.ts +23 -3
  104. package/dist/npm/installer.d.ts.map +1 -1
  105. package/dist/npm/installer.js +330 -131
  106. package/dist/npm/npx-install.d.ts +3 -1
  107. package/dist/npm/npx-install.d.ts.map +1 -1
  108. package/dist/npm/npx-install.js +4 -1
  109. package/dist/npm/package-lock.d.ts +32 -0
  110. package/dist/npm/package-lock.d.ts.map +1 -0
  111. package/dist/npm/package-lock.js +65 -0
  112. package/dist/npm/placement.d.ts +16 -0
  113. package/dist/npm/placement.d.ts.map +1 -0
  114. package/dist/npm/placement.js +33 -0
  115. package/dist/npm/pre-bundle-facet.d.ts +1 -30
  116. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  117. package/dist/npm/pre-bundle-facet.js +3 -5
  118. package/dist/npm/r2-cache.d.ts +12 -6
  119. package/dist/npm/r2-cache.d.ts.map +1 -1
  120. package/dist/npm/r2-cache.js +34 -16
  121. package/dist/npm/resolve-one-facet.d.ts +3 -0
  122. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  123. package/dist/npm/resolve-one-facet.js +15 -3
  124. package/dist/npm/resolver.d.ts +11 -13
  125. package/dist/npm/resolver.d.ts.map +1 -1
  126. package/dist/npm/resolver.js +12 -25
  127. package/dist/opencode-artifact.generated.js +2 -2
  128. package/dist/replica/routing.d.ts +2 -38
  129. package/dist/replica/routing.d.ts.map +1 -1
  130. package/dist/replica/routing.js +3 -55
  131. package/dist/router/index.d.ts.map +1 -1
  132. package/dist/router/index.js +15 -2
  133. package/dist/router/public-directory.d.ts +18 -0
  134. package/dist/router/public-directory.d.ts.map +1 -1
  135. package/dist/router/public-directory.js +1 -1
  136. package/dist/router/remote-api.d.ts +3 -1
  137. package/dist/router/remote-api.d.ts.map +1 -1
  138. package/dist/router/remote-api.js +42 -79
  139. package/dist/runtime/bash-repl.js +2 -2
  140. package/dist/runtime/bun-repl.d.ts.map +1 -1
  141. package/dist/runtime/bun-repl.js +2 -13
  142. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  143. package/dist/runtime/cpython-resident.js +2 -10
  144. package/dist/runtime/esbuild-wasm-bytes.d.ts +37 -18
  145. package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
  146. package/dist/runtime/esbuild-wasm-bytes.js +66 -92
  147. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  148. package/dist/runtime/facet-loader-host.js +0 -7
  149. package/dist/runtime/git-bundle-artifact.d.ts +17 -0
  150. package/dist/runtime/git-bundle-artifact.d.ts.map +1 -0
  151. package/dist/runtime/git-bundle-artifact.js +25 -0
  152. package/dist/runtime/node-repl.d.ts.map +1 -1
  153. package/dist/runtime/node-repl.js +2 -13
  154. package/dist/runtime/node-shims-artifact.d.ts +22 -23
  155. package/dist/runtime/node-shims-artifact.d.ts.map +1 -1
  156. package/dist/runtime/node-shims-artifact.js +48 -78
  157. package/dist/runtime/node-shims.d.ts.map +1 -1
  158. package/dist/runtime/node-shims.js +2358 -996
  159. package/dist/runtime/opencode-artifact.d.ts.map +1 -1
  160. package/dist/runtime/opencode-artifact.js +17 -53
  161. package/dist/runtime/opencode-facet-runner.d.ts +3 -6
  162. package/dist/runtime/opencode-facet-runner.d.ts.map +1 -1
  163. package/dist/runtime/opencode-facet-runner.js +45 -10
  164. package/dist/runtime/opentui-wasm-bytes.d.ts.map +1 -1
  165. package/dist/runtime/opentui-wasm-bytes.js +16 -49
  166. package/dist/runtime/package-manager.d.ts +0 -2
  167. package/dist/runtime/package-manager.d.ts.map +1 -1
  168. package/dist/runtime/package-manager.js +0 -3
  169. package/dist/runtime/python-repl.d.ts.map +1 -1
  170. package/dist/runtime/python-repl.js +4 -35
  171. package/dist/runtime/repl-session.d.ts +10 -0
  172. package/dist/runtime/repl-session.d.ts.map +1 -1
  173. package/dist/runtime/repl-session.js +7 -0
  174. package/dist/runtime/ruby-repl.d.ts.map +1 -1
  175. package/dist/runtime/ruby-repl.js +2 -2
  176. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  177. package/dist/runtime/ruby-resident.js +41 -51
  178. package/dist/runtime/runtime-catalog.d.ts +10 -3
  179. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  180. package/dist/runtime/runtime-catalog.js +125 -15
  181. package/dist/runtime/sqlite-wasm-bytes.d.ts +4 -4
  182. package/dist/runtime/sqlite-wasm-bytes.d.ts.map +1 -1
  183. package/dist/runtime/sqlite-wasm-bytes.js +21 -57
  184. package/dist/runtime/staged-source.d.ts +67 -0
  185. package/dist/runtime/staged-source.d.ts.map +1 -0
  186. package/dist/runtime/staged-source.js +113 -0
  187. package/dist/runtime/wasi/preamble.d.ts +0 -22
  188. package/dist/runtime/wasi/preamble.d.ts.map +1 -1
  189. package/dist/runtime/wasi/preamble.js +98 -1425
  190. package/dist/runtime-catalog.generated.js +1 -1
  191. package/dist/session/agent.d.ts +14 -2
  192. package/dist/session/agent.d.ts.map +1 -1
  193. package/dist/session/agent.js +57 -19
  194. package/dist/session/ai.d.ts +0 -11
  195. package/dist/session/ai.d.ts.map +1 -1
  196. package/dist/session/ai.js +0 -4
  197. package/dist/session/fs-watch.d.ts +0 -11
  198. package/dist/session/fs-watch.d.ts.map +1 -1
  199. package/dist/session/fs-watch.js +0 -21
  200. package/dist/session/hibernation.d.ts +68 -2
  201. package/dist/session/hibernation.d.ts.map +1 -1
  202. package/dist/session/hibernation.js +95 -1
  203. package/dist/session/init-phases.d.ts +1 -2
  204. package/dist/session/init-phases.d.ts.map +1 -1
  205. package/dist/session/init-phases.js +1 -2
  206. package/dist/session/init.d.ts.map +1 -1
  207. package/dist/session/init.js +7 -20
  208. package/dist/session/keys.d.ts +0 -3
  209. package/dist/session/keys.d.ts.map +1 -1
  210. package/dist/session/keys.js +0 -3
  211. package/dist/session/legacy-reset.d.ts +11 -0
  212. package/dist/session/legacy-reset.d.ts.map +1 -0
  213. package/dist/session/legacy-reset.js +16 -0
  214. package/dist/session/nimbus-session.d.ts +50 -16
  215. package/dist/session/nimbus-session.d.ts.map +1 -1
  216. package/dist/session/nimbus-session.js +77 -71
  217. package/dist/session/npm-install-port.d.ts.map +1 -1
  218. package/dist/session/npm-install-port.js +2 -0
  219. package/dist/session/programmatic.d.ts +13 -18
  220. package/dist/session/programmatic.d.ts.map +1 -1
  221. package/dist/session/programmatic.js +53 -39
  222. package/dist/session/replica-routes.d.ts +2 -3
  223. package/dist/session/replica-routes.d.ts.map +1 -1
  224. package/dist/session/replica-routes.js +3 -10
  225. package/dist/session/routes.d.ts.map +1 -1
  226. package/dist/session/routes.js +11 -12
  227. package/dist/session/rpc.d.ts +108 -21
  228. package/dist/session/rpc.d.ts.map +1 -1
  229. package/dist/session/rpc.js +194 -39
  230. package/dist/session/start-real-vite.js +2 -2
  231. package/dist/session/state-store.d.ts +1 -17
  232. package/dist/session/state-store.d.ts.map +1 -1
  233. package/dist/session/state-store.js +3 -53
  234. package/dist/session/supervisor-op.d.ts +7 -0
  235. package/dist/session/supervisor-op.d.ts.map +1 -1
  236. package/dist/session/supervisor-op.js +6 -1
  237. package/dist/session/supervisor-rpc.d.ts +116 -19
  238. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  239. package/dist/session/supervisor-rpc.js +194 -84
  240. package/dist/session/vite-command.d.ts.map +1 -1
  241. package/dist/session/vite-command.js +17 -14
  242. package/dist/session/ws.d.ts.map +1 -1
  243. package/dist/session/ws.js +20 -8
  244. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  245. package/dist/shell/npm-bin-entrypoints.js +30 -5
  246. package/dist/vfs/facet-resident-limits.d.ts +33 -0
  247. package/dist/vfs/facet-resident-limits.d.ts.map +1 -0
  248. package/dist/vfs/facet-resident-limits.js +33 -0
  249. package/dist/vfs/facet-resident-store.d.ts +2 -10
  250. package/dist/vfs/facet-resident-store.d.ts.map +1 -1
  251. package/dist/vfs/facet-resident-store.js +1594 -219
  252. package/dist/workspace-host.d.ts +3 -1
  253. package/dist/workspace-host.d.ts.map +1 -1
  254. package/dist/workspace-host.js +2 -0
  255. package/dist/wrangler/nimbus-wrangler.d.ts +0 -2
  256. package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -1
  257. package/dist/wrangler/nimbus-wrangler.js +8 -22
  258. package/package.json +34 -7
  259. package/public/_assets/esbuild-0.24.2.js +2380 -0
  260. package/public/_assets/opencode/1.16.2/index-attach.js +2 -2
  261. package/public/_assets/runtime/esbuild-cli-c4fda380d52befc1.js +6892 -0
  262. package/public/_assets/runtime/git-de3e60c9f3ff006a.js +103 -0
  263. package/public/_assets/runtime/{node-shims-acaa037e6d26ec0d.js → node-shims-06565b121555c194.js} +2879 -992
  264. package/public/_assets/runtime/resident-store-6ffb1939c53d982f.js +2264 -0
  265. package/public/_assets/runtime/vfs-write-ledger-e501e8fc793bc2c1.js +889 -0
  266. package/scripts/build-opencode-attach-entry.mjs +51 -1
  267. package/scripts/bundle-esbuild-wasm.mjs +55 -39
  268. package/scripts/bundle-facet-workers.mjs +159 -10
  269. package/scripts/bundle-git.mjs +88 -27
  270. package/scripts/bundle-node-shims.mjs +72 -46
  271. package/scripts/bundle-runtime.mjs +18 -421
  272. package/scripts/cf-git-patch.mjs +137 -0
  273. package/scripts/patch-install-deps.mjs +49 -96
  274. package/scripts/runtime-specs.mjs +435 -0
  275. package/vendor/git-http.generated.d.ts +82 -0
  276. package/vendor/git-upstream.generated.d.ts +4378 -0
  277. package/vendor/git.LICENSE +7 -0
  278. package/vendor/git.generated.d.mts +21 -0
  279. package/vendor/git.generated.mjs +103 -0
  280. package/dist/replica/suspension.d.ts +0 -36
  281. package/dist/replica/suspension.d.ts.map +0 -1
  282. package/dist/replica/suspension.js +0 -49
  283. package/dist/runtime/static-server.d.ts +0 -3
  284. package/dist/runtime/static-server.d.ts.map +0 -1
  285. package/dist/runtime/static-server.js +0 -133
  286. package/scripts/clean-dist.mjs +0 -7
@@ -1,5 +1,5 @@
1
1
  /**
2
- * npm-installer.ts — Unified npm installer for Nimbus.
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 { packumentUrl } from './r2-cache.js';
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
- const { specs, devOnly, optionalRoots, advised } = await this.buildSpecs(projDir, opts?.packages, opts?.production);
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
- if (Object.keys(specs).length === 0) {
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 (lockfile && !opts?.packages && this.isLockfileValid(lockfile, specs)) {
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 [, pkg] of resolved) {
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 + '/' + pkg.name + '/package.json';
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(pkg);
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, hoistPlan, nmDir, opts?.pid);
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, resolved, hoistPlan, nmDir);
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
- log(`Done! ${installed.length} packages, ${totalFiles} files in ${(elapsed / 1000).toFixed(1)}s`);
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
- * - cycle detection (`seen`),
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
- let queue = Object.entries(specs);
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
- // Dedupe + filter the layer up front. The task body also filters
548
- // (defensive), but doing it here avoids dispatching wasted RPC
549
- // for already-seen names.
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 layerSeenLocal = new Set();
552
- for (const [name, range] of queue) {
553
- if (seen.has(name) || layerSeenLocal.has(name))
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
- layerSeenLocal.add(name);
556
- seen.add(name);
557
- layer.push([name, range]);
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} seen=${seen.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(([name, range]) => {
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: name, args: taskSpec };
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 [taskName] = layer[i];
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
- if (resolved.has(pkg.name))
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
- resolved.set(pkg.name, pkg);
722
- // Edge extraction.
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([depName, depRange]);
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([depName, depRange]);
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([peerName, peerRange]);
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([peerName, peerRange]);
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 = resolved.get(name);
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, hoistPlan, nmDir, pid) {
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 + '/' + p.name,
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
- const shardCount = Math.min(specs.length, INSTALL_PEER_CAP);
903
- // Round-robin assignment: spec at pkgIdx → shard pkgIdx % shardCount.
904
- // This produces ⌈specs.length / shardCount⌉ specs per shard at
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
- specs.forEach((spec, idx) => {
908
- shards[idx % shardCount].push(spec);
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 specByPackage = new Map(specs.map((s) => [`${s.name}@${s.version}`, s]));
1051
+ const specByDir = new Map(specs.map((s) => [s.pkgDir, s]));
971
1052
  for (const r of result.perPackage) {
972
- reported.add(`${r.name}@${r.version}`);
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 = specByPackage.get(`${r.name}@${r.version}`);
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
- const id = `${s.name}@${s.version}`;
1000
- if (reported.has(id))
1082
+ if (reported.has(s.pkgDir))
1001
1083
  continue;
1002
- failed.push(id);
1084
+ failed.push(`${s.name}@${s.version}`);
1003
1085
  failCount++;
1004
- log(` [warn] ${id}: install shard returned no result for this package`);
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
- for (const [, entry] of lockfile) {
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(depName))
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(peerName))
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
- * Convert a lockfile back to resolved packages (for cache restore).
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
- lockfileToResolved(lockfile) {
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
- for (const [name, entry] of lockfile) {
1195
- // Reconstruct from registry cache
1196
- const cached = this.cache.getRegistryEntry(name, entry.resolvedVer);
1197
- if (cached) {
1198
- resolved.set(name, {
1199
- name: cached.name,
1200
- version: cached.version,
1201
- tarballUrl: cached.tarballUrl,
1202
- integrity: cached.integrity,
1203
- dependencies: safeJsonParse(cached.depsJson, {}),
1204
- exports: safeJsonParse(cached.exportsJson, null),
1205
- main: cached.main,
1206
- module: cached.moduleField,
1207
- bin: safeJsonParse(cached.binJson, {}),
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
- else {
1211
- // Registry cache miss — create minimal entry
1212
- resolved.set(name, {
1213
- name,
1214
- version: entry.resolvedVer,
1215
- tarballUrl: '',
1216
- integrity: entry.integrity,
1217
- dependencies: safeJsonParse(entry.depsJson, {}),
1218
- exports: null,
1219
- main: '',
1220
- module: '',
1221
- bin: {},
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, resolved, _hoistPlan, nmDir) {
1438
+ writeLockfile(projDir, hoistPlan, nmDir) {
1231
1439
  const entries = new Map();
1232
- for (const [name, pkg] of resolved) {
1233
- entries.set(name, {
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 + '/' + name,
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
- // Find the resolved package matching this spec
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
- // W6: if a swap fired, the user typed `name` but `resolved` is
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
  }