@nimbus-sh/worker 0.8.0 → 0.10.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 (124) hide show
  1. package/dist/esbuild-wasm-bundle.generated.d.ts +24 -24
  2. package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -1
  3. package/dist/esbuild-wasm-bundle.generated.js +24 -24
  4. package/dist/facets/cirrus-real.js +3 -3
  5. package/dist/facets/compose.d.ts +3 -0
  6. package/dist/facets/compose.d.ts.map +1 -1
  7. package/dist/facets/compose.js +10 -3
  8. package/dist/facets/esbuild-bundle-pool.d.ts +1 -1
  9. package/dist/facets/esbuild-bundle-pool.js +3 -3
  10. package/dist/facets/esbuild-transform.d.ts +6 -2
  11. package/dist/facets/esbuild-transform.d.ts.map +1 -1
  12. package/dist/facets/esbuild-transform.js +8 -4
  13. package/dist/facets/manager.d.ts +54 -15
  14. package/dist/facets/manager.d.ts.map +1 -1
  15. package/dist/facets/manager.js +227 -144
  16. package/dist/facets/real-vite-hmr.d.ts +2 -0
  17. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  18. package/dist/facets/real-vite-hmr.js +3 -2
  19. package/dist/git/commands.d.ts +17 -1
  20. package/dist/git/commands.d.ts.map +1 -1
  21. package/dist/git/commands.js +73 -9
  22. package/dist/git/network-facet.d.ts +2 -0
  23. package/dist/git/network-facet.d.ts.map +1 -1
  24. package/dist/git/network-facet.js +10 -5
  25. package/dist/hosted/commands.d.ts.map +1 -1
  26. package/dist/hosted/commands.js +28 -39
  27. package/dist/hosted/runtime.d.ts +24 -2
  28. package/dist/hosted/runtime.d.ts.map +1 -1
  29. package/dist/hosted/runtime.js +70 -13
  30. package/dist/hosted/services.d.ts +14 -2
  31. package/dist/hosted/services.d.ts.map +1 -1
  32. package/dist/hosted/services.js +15 -2
  33. package/dist/loaders/generated-workers.d.ts +2 -2
  34. package/dist/loaders/generated-workers.d.ts.map +1 -1
  35. package/dist/loaders/generated-workers.js +4 -4
  36. package/dist/loaders/pre-bundle-preamble.d.ts +6 -3
  37. package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
  38. package/dist/loaders/pre-bundle-preamble.js +13 -8
  39. package/dist/node-shims-artifact.generated.js +3 -3
  40. package/dist/npm/cache.d.ts +6 -1
  41. package/dist/npm/cache.d.ts.map +1 -1
  42. package/dist/npm/cache.js +8 -6
  43. package/dist/npm/install-batch-facet.d.ts +2 -0
  44. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  45. package/dist/npm/install-batch-facet.js +29 -15
  46. package/dist/npm/installer.d.ts +8 -4
  47. package/dist/npm/installer.d.ts.map +1 -1
  48. package/dist/npm/installer.js +173 -113
  49. package/dist/npm/npx-install.d.ts +3 -1
  50. package/dist/npm/npx-install.d.ts.map +1 -1
  51. package/dist/npm/npx-install.js +4 -1
  52. package/dist/npm/placement.d.ts +16 -0
  53. package/dist/npm/placement.d.ts.map +1 -0
  54. package/dist/npm/placement.js +33 -0
  55. package/dist/npm/r2-cache.d.ts +10 -6
  56. package/dist/npm/r2-cache.d.ts.map +1 -1
  57. package/dist/npm/r2-cache.js +32 -16
  58. package/dist/npm/resolve-one-facet.d.ts +3 -0
  59. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  60. package/dist/npm/resolve-one-facet.js +1 -0
  61. package/dist/npm/resolver.d.ts +11 -6
  62. package/dist/npm/resolver.d.ts.map +1 -1
  63. package/dist/npm/resolver.js +12 -3
  64. package/dist/runtime/bash-repl.d.ts +5 -3
  65. package/dist/runtime/bash-repl.d.ts.map +1 -1
  66. package/dist/runtime/bash-repl.js +8 -3
  67. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  68. package/dist/runtime/cpython-resident.js +2 -10
  69. package/dist/runtime/esbuild-wasm-bytes.d.ts +25 -18
  70. package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
  71. package/dist/runtime/esbuild-wasm-bytes.js +55 -30
  72. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  73. package/dist/runtime/facet-loader-host.js +0 -9
  74. package/dist/runtime/node-shims.d.ts.map +1 -1
  75. package/dist/runtime/node-shims.js +853 -181
  76. package/dist/runtime/python-repl.d.ts +4 -3
  77. package/dist/runtime/python-repl.d.ts.map +1 -1
  78. package/dist/runtime/python-repl.js +10 -39
  79. package/dist/runtime/ruby-repl.d.ts +3 -2
  80. package/dist/runtime/ruby-repl.d.ts.map +1 -1
  81. package/dist/runtime/ruby-repl.js +7 -6
  82. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  83. package/dist/runtime/ruby-resident.js +5 -16
  84. package/dist/runtime/runtime-catalog.js +1 -1
  85. package/dist/runtime/wasi/preamble.d.ts +0 -22
  86. package/dist/runtime/wasi/preamble.d.ts.map +1 -1
  87. package/dist/runtime/wasi/preamble.js +160 -1445
  88. package/dist/runtime-catalog.generated.js +1 -1
  89. package/dist/session/hibernation.d.ts +45 -2
  90. package/dist/session/hibernation.d.ts.map +1 -1
  91. package/dist/session/hibernation.js +78 -1
  92. package/dist/session/init.d.ts.map +1 -1
  93. package/dist/session/init.js +1 -0
  94. package/dist/session/nimbus-session.d.ts +16 -23
  95. package/dist/session/nimbus-session.d.ts.map +1 -1
  96. package/dist/session/nimbus-session.js +25 -36
  97. package/dist/session/npm-install-port.d.ts.map +1 -1
  98. package/dist/session/npm-install-port.js +1 -0
  99. package/dist/session/port-capability.d.ts.map +1 -1
  100. package/dist/session/port-capability.js +11 -1
  101. package/dist/session/programmatic.js +2 -2
  102. package/dist/session/routes.d.ts.map +1 -1
  103. package/dist/session/routes.js +4 -2
  104. package/dist/session/rpc.d.ts +7 -44
  105. package/dist/session/rpc.d.ts.map +1 -1
  106. package/dist/session/rpc.js +17 -67
  107. package/dist/session/supervisor-op.d.ts +8 -3
  108. package/dist/session/supervisor-op.d.ts.map +1 -1
  109. package/dist/session/supervisor-op.js +10 -45
  110. package/dist/session/supervisor-rpc.d.ts +47 -22
  111. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  112. package/dist/session/supervisor-rpc.js +64 -26
  113. package/dist/vfs/facet-resident-store.d.ts.map +1 -1
  114. package/dist/vfs/facet-resident-store.js +4 -2
  115. package/dist/workspace-host.d.ts +2 -1
  116. package/dist/workspace-host.d.ts.map +1 -1
  117. package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -1
  118. package/dist/wrangler/nimbus-wrangler.js +3 -0
  119. package/package.json +30 -4
  120. package/public/_assets/esbuild-0.24.2.js +2380 -0
  121. package/public/_assets/runtime/{node-shims-8f8c64edd2a1f0f5.js → node-shims-7b4dd7a76d95d21e.js} +852 -180
  122. package/scripts/bundle-esbuild-wasm.mjs +55 -39
  123. package/scripts/bundle-facet-workers.mjs +45 -7
  124. package/scripts/bundle-runtime.mjs +35 -8
@@ -22,8 +22,9 @@
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 { npmRegistryOrigin, packumentUrl } from './r2-cache.js';
27
28
  import { satisfiesRange, isSemverRange } from './semver.js';
28
29
  import { npmAddedLine, npmHttpCacheLine, npmHttpFetchLine, npmTitleLine, } from '@nimbus-sh/core/substrate/lifo/commands/system/npm-log.js';
29
30
  import { applySwaps, findRejects, lookupSwap, isOptionalNativeBinding, formatSwapNotice, emitRegistryEvent, } from '../facets/wasm-swap-registry.js';
@@ -115,6 +116,7 @@ export class NpmInstaller {
115
116
  }
116
117
  }
117
118
  async _installInner(projDir, nmDir, opts, log, start) {
119
+ const registry = npmRegistryOrigin(opts?.registry);
118
120
  const phases = {};
119
121
  const installed = [];
120
122
  const failed = [];
@@ -143,10 +145,11 @@ export class NpmInstaller {
143
145
  }
144
146
  const lockfile = this.cache.readLockfile(projDir);
145
147
  let resolved;
148
+ let nested;
146
149
  let usedLockfile = false;
147
150
  if (lockfile && !opts?.packages && this.isLockfileValid(lockfile, specs)) {
148
151
  log(`Lockfile valid (${lockfile.size} packages). Skipping resolution.`);
149
- resolved = this.lockfileToResolved(lockfile);
152
+ ({ resolved, nested } = this.lockfileToResolved(lockfile));
150
153
  usedLockfile = true;
151
154
  phases['lock-check'] = Date.now() - phaseStart;
152
155
  }
@@ -166,8 +169,9 @@ export class NpmInstaller {
166
169
  phaseStart = Date.now();
167
170
  setInstallPhase('resolve');
168
171
  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 });
172
+ const tree = await this.resolveTreeViaFanout(specs, log, { optionalRoots, devOnly, advised, registry });
170
173
  resolved = tree.resolved;
174
+ nested = tree.nested;
171
175
  phases['resolve'] = Date.now() - phaseStart;
172
176
  // Transitive platform-gate refusals. Required ones join `failed`
173
177
  // with their reason; optional-edge ones stay out of the outcome
@@ -197,13 +201,15 @@ export class NpmInstaller {
197
201
  // ── Phase 2: Hoist ────────────────────────────────────────────────
198
202
  phaseStart = Date.now();
199
203
  setInstallPhase('hoist');
200
- const hoistPlan = computeHoistPlan(resolved);
204
+ const hoistPlan = computeHoistPlan(resolved, nested);
201
205
  phases['hoist'] = Date.now() - phaseStart;
202
206
  // ── Phase 3: Diff (cache check) ─────────────────────────────────
207
+ // Per placement: a nested copy is checked at its own directory.
203
208
  phaseStart = Date.now();
204
209
  setInstallPhase('diff');
205
210
  const toFetch = [];
206
- for (const [, pkg] of resolved) {
211
+ for (const target of hoistPlacements(hoistPlan)) {
212
+ const { pkg } = target;
207
213
  if (!pkg.tarballUrl) {
208
214
  // Resolved metadata with no tarball cannot be installed. Silently
209
215
  // dropping it here left the package absent from node_modules and
@@ -213,7 +219,7 @@ export class NpmInstaller {
213
219
  continue;
214
220
  }
215
221
  // Check if already installed at the correct path
216
- const pkgJsonPath = nmDir + '/' + pkg.name + '/package.json';
222
+ const pkgJsonPath = nmDir + '/' + target.placement + '/package.json';
217
223
  if (this.vfs.exists(pkgJsonPath)) {
218
224
  try {
219
225
  const existing = JSON.parse(this.vfs.readFileString(pkgJsonPath));
@@ -229,7 +235,7 @@ export class NpmInstaller {
229
235
  // Not present in the VFS at the right version — fetch it. The batch
230
236
  // facet consults the shared L2 (caches.default) + L3 (R2) tarball
231
237
  // tiers itself, so a cross-DO warm cache still lands here.
232
- toFetch.push(pkg);
238
+ toFetch.push(target);
233
239
  }
234
240
  phases['diff'] = Date.now() - phaseStart;
235
241
  log(`To fetch: ${toFetch.length}, already installed: ${cachedHits}`);
@@ -257,7 +263,7 @@ export class NpmInstaller {
257
263
  const commitsBefore = this.storageCommitCount();
258
264
  if (toFetch.length > 0) {
259
265
  log(`Fetching ${toFetch.length} packages... (path: batch-facet)`);
260
- const batchResult = await this.fetchViaBatchFacet(toFetch, hoistPlan, nmDir, opts?.pid);
266
+ const batchResult = await this.fetchViaBatchFacet(toFetch, nmDir, opts?.pid);
261
267
  totalFiles += batchResult.filesWritten;
262
268
  for (const name of batchResult.installed)
263
269
  installed.push(name);
@@ -273,7 +279,7 @@ export class NpmInstaller {
273
279
  phases['link-bins'] = Date.now() - phaseStart;
274
280
  // ── Write lockfile ──────────────────────────────────────────────
275
281
  if (!usedLockfile || opts?.packages) {
276
- this.writeLockfile(projDir, resolved, hoistPlan, nmDir);
282
+ this.writeLockfile(projDir, hoistPlan, nmDir);
277
283
  }
278
284
  // ── Update package.json if explicit packages were added ─────────
279
285
  if (opts?.packages && opts.packages.length > 0) {
@@ -400,7 +406,8 @@ export class NpmInstaller {
400
406
  `${failed.length} missing: ${failed.join(', ')}`);
401
407
  }
402
408
  else {
403
- log(`Done! ${installed.length} packages, ${totalFiles} files in ${(elapsed / 1000).toFixed(1)}s`);
409
+ const nestedNote = hoistPlan.nested.size > 0 ? ` (${hoistPlan.nested.size} nested)` : '';
410
+ log(`Done! ${installed.length} packages${nestedNote}, ${totalFiles} files in ${(elapsed / 1000).toFixed(1)}s`);
404
411
  // npm's own summary line, unstyled: the styled one the command site
405
412
  // prints carries a colour prefix that no log parser matches. Carried
406
413
  // at `http` — the level from which this stream is machine-readable —
@@ -442,8 +449,13 @@ export class NpmInstaller {
442
449
  * width < IN_DO_THRESHOLD (5) → in-DO fanout in-DO loader-pool
443
450
  * width >= IN_DO_THRESHOLD → peer-DO fanout peer-DO (sibling NimbusSession DOs)
444
451
  *
452
+ * Resolution is per edge, not per name: the first version of a name goes
453
+ * to root; a later edge the nearest visible placement does not satisfy
454
+ * nests a copy under its dependent, never higher (placement.ts; live case
455
+ * in tests/unit/npm-install-nested-conflict.mjs).
456
+ *
445
457
  * The supervisor still owns:
446
- * - cycle detection (`seen`),
458
+ * - placement (`placed` / `pending` / `settled`, by placement path),
447
459
  * - X.5-F top-level / required-peer policy,
448
460
  * - X.5-G G1 optional-native silent-skip,
449
461
  * - X.5-drizzle best-effort tagging on optional-peer subtrees,
@@ -465,6 +477,7 @@ export class NpmInstaller {
465
477
  */
466
478
  async resolveTreeViaFanout(specs, log, opts = {}) {
467
479
  const t0 = Date.now();
480
+ const registry = npmRegistryOrigin(opts.registry);
468
481
  const optionalRoots = opts.optionalRoots ?? new Set();
469
482
  const devOnly = opts.devOnly ?? new Set();
470
483
  // Policy advisories already announced — one `note:` line per name
@@ -479,21 +492,27 @@ export class NpmInstaller {
479
492
  log(`[npm] note: ${ev.from} has no Workers-compatible build${marker}: ${ev.reason}${hint}`);
480
493
  };
481
494
  const __f2Diag = (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1');
482
- // Per-walk state — supervisor side.
495
+ // Per-walk state — supervisor side. Root by name, nested by placement
496
+ // path, `placed` is both; `pending` is dispatched and unanswered,
497
+ // `settled` answered whatever the answer was.
483
498
  const resolved = new Map();
499
+ const nested = new Map();
500
+ const placed = new Map();
501
+ const pending = new Set();
502
+ const settled = new Set();
484
503
  // 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
504
  const unresolved = new Map();
488
505
  // EBADPLATFORM refusals (os/cpu/libc allowlists), in walk order.
489
506
  // Announced as `[skip]` lines at record time; required-vs-optional is
490
507
  // classified at end of walk, when every required edge has been seen.
491
508
  const refusals = new Map();
492
- const seen = new Set();
493
509
  const topLevelNames = new Set(Object.keys(specs));
494
510
  const optionalNames = new Set(); // X.5-G G1
495
511
  const bestEffortNames = new Set(); // X.5-drizzle
496
- let queue = Object.entries(specs);
512
+ // A cycle with mutually incompatible ranges has no finite layout; the
513
+ // edge is refused at this depth rather than nested without end.
514
+ const MAX_NEST_DEPTH = 16;
515
+ let queue = Object.entries(specs).map(([name, range]) => ({ name, range, from: '', kind: 'dep' }));
497
516
  const cacheWritesPending = [];
498
517
  let totalPackumentBytes = 0;
499
518
  let totalPackumentsDecoded = 0;
@@ -544,21 +563,53 @@ export class NpmInstaller {
544
563
  // Frontier loop. Each iteration = ONE BFS layer dispatched as ONE
545
564
  // submitMany batch.
546
565
  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.
566
+ // Decide each edge against what Node's walk from its dependent sees.
567
+ // One whose nearest visible placement is in flight waits a layer.
568
+ // A copy nested under a package is decided here in the layer after
569
+ // that package resolves, before anything beneath it can resolve, so
570
+ // no descendant is ever decided against a copy it will later shadow.
550
571
  const layer = [];
551
- const layerSeenLocal = new Set();
552
- for (const [name, range] of queue) {
553
- if (seen.has(name) || layerSeenLocal.has(name))
572
+ const deferred = [];
573
+ for (const edge of queue) {
574
+ const req = parseRegistryRequest(edge.name, edge.range);
575
+ let target = edge.name;
576
+ let decided = false;
577
+ for (const candidate of visiblePlacements(edge.from, edge.name)) {
578
+ if (pending.has(candidate)) {
579
+ deferred.push(edge);
580
+ decided = true;
581
+ break;
582
+ }
583
+ const pkg = placed.get(candidate);
584
+ if (!pkg)
585
+ continue; // never asked or failed: the walk goes on upward
586
+ if (!isSemverRange(req.range) || satisfiesRange(pkg.version, req.range)) {
587
+ decided = true; // reuse
588
+ }
589
+ else if (edge.kind === 'peer') {
590
+ decided = true; // reuse anyway, and say so the way npm does
591
+ log(` [warn] peer ${edge.name}@${edge.range} from ${edge.from} is met by ${pkg.name}@${pkg.version} (${candidate})`);
592
+ }
593
+ else {
594
+ target = nestedPlacement(edge.from, edge.name);
595
+ }
596
+ break;
597
+ }
598
+ if (decided)
599
+ continue;
600
+ if (settled.has(target))
601
+ continue; // failed once, on record under the name
602
+ if (target.split('/node_modules/').length - 1 > MAX_NEST_DEPTH) {
603
+ unresolved.set(edge.name, `${edge.name}@${edge.range} from ${edge.from} would nest deeper than ${MAX_NEST_DEPTH} levels (cycle with incompatible ranges)`);
604
+ settled.add(target);
554
605
  continue;
555
- layerSeenLocal.add(name);
556
- seen.add(name);
557
- layer.push([name, range]);
606
+ }
607
+ pending.add(target);
608
+ layer.push({ edge, placement: target });
558
609
  }
559
- queue = [];
610
+ queue = deferred;
560
611
  if (__f2Diag) {
561
- log(`[f2-frontier] N=${layerN} width=${layer.length} resolved-so-far=${resolved.size} seen=${seen.size}`);
612
+ log(`[f2-frontier] N=${layerN} width=${layer.length} resolved-so-far=${resolved.size} settled=${settled.size}`);
562
613
  }
563
614
  if (layer.length === 0)
564
615
  break;
@@ -570,7 +621,7 @@ export class NpmInstaller {
570
621
  // cache slice (only entries for THIS name) so the per-task RPC
571
622
  // stays small. Bounded to 16 versions per name — enough to cover
572
623
  // ~2 majors of typical packages, well under any RPC arg size cap.
573
- const tasks = layer.map(([name, range]) => {
624
+ const tasks = layer.map(({ edge: { name, range }, placement }) => {
574
625
  const cachedRows = this.cache.getRegistryVersions(name).slice(0, 16);
575
626
  const cachedEntries = cachedRows.map((e) => ({
576
627
  name: e.name,
@@ -594,8 +645,9 @@ export class NpmInstaller {
594
645
  isOptional: optionalNames.has(name),
595
646
  fetchTimeoutMs: 15_000,
596
647
  retries: 3,
648
+ registry,
597
649
  };
598
- return { key: name, args: taskSpec };
650
+ return { key: placement, args: taskSpec };
599
651
  });
600
652
  // Dispatch the layer. Fanout routes:
601
653
  // <5 → in-DO fanout in-DO (IsolatePool), concurrency = layer.length (capped at 4)
@@ -617,7 +669,10 @@ export class NpmInstaller {
617
669
  // fanout failed to deliver is recorded instead of skipped.
618
670
  for (let i = 0; i < layer.length; i++) {
619
671
  const res = results[i];
620
- const [taskName] = layer[i];
672
+ const { edge, placement } = layer[i];
673
+ const taskName = edge.name;
674
+ pending.delete(placement);
675
+ settled.add(placement);
621
676
  if (!res) {
622
677
  unresolved.set(taskName, `resolver fanout returned no result at layer ${layerN}`);
623
678
  continue;
@@ -643,10 +698,10 @@ export class NpmInstaller {
643
698
  // One `npm http` line per packument, reporting the tier that actually
644
699
  // served it. A skipped task issued no request, so it gets no line.
645
700
  if (res.packumentSource === 'network') {
646
- this.npmLog('http', npmHttpFetchLine(packumentUrl(taskName), res.packumentElapsedMs));
701
+ this.npmLog('http', npmHttpFetchLine(packumentUrl(taskName, registry), res.packumentElapsedMs));
647
702
  }
648
703
  else if (res.packumentSource !== 'skipped') {
649
- this.npmLog('http', npmHttpCacheLine(packumentUrl(taskName)));
704
+ this.npmLog('http', npmHttpCacheLine(packumentUrl(taskName, registry)));
650
705
  }
651
706
  totalPackumentBytes += res.packumentBytesDecoded;
652
707
  if (res.packumentSource === 'r2-cache')
@@ -716,37 +771,39 @@ export class NpmInstaller {
716
771
  continue;
717
772
  }
718
773
  }
719
- if (resolved.has(pkg.name))
774
+ // A W6 swap answers with another name; place what came back.
775
+ const isRoot = placement === taskName;
776
+ const actual = isRoot ? pkg.name : nestedPlacement(edge.from, pkg.name);
777
+ if (placed.has(actual))
720
778
  continue;
721
- resolved.set(pkg.name, pkg);
722
- // Edge extraction.
779
+ placed.set(actual, pkg);
780
+ settled.add(actual);
781
+ if (isRoot)
782
+ resolved.set(pkg.name, pkg);
783
+ else
784
+ nested.set(actual, pkg);
785
+ // Edge extraction, from this package's own placement.
723
786
  const inheritBestEffort = bestEffortNames.has(pkg.name);
724
787
  for (const [depName, depRange] of Object.entries(pkg.dependencies)) {
725
- if (resolved.has(depName) || seen.has(depName))
726
- continue;
727
788
  if (inheritBestEffort)
728
789
  bestEffortNames.add(depName);
729
- queue.push([depName, depRange]);
790
+ queue.push({ name: depName, range: depRange, from: actual, kind: 'dep' });
730
791
  }
731
792
  const optDeps = pkg.optionalDependencies;
732
793
  if (optDeps) {
733
794
  for (const [depName, depRange] of Object.entries(optDeps)) {
734
- if (resolved.has(depName) || seen.has(depName))
735
- continue;
736
795
  optionalNames.add(depName);
737
796
  if (inheritBestEffort)
738
797
  bestEffortNames.add(depName);
739
- queue.push([depName, depRange]);
798
+ queue.push({ name: depName, range: depRange, from: actual, kind: 'dep' });
740
799
  }
741
800
  }
742
801
  if (pkg.peerDependencies) {
743
802
  for (const [peerName, peerRange] of Object.entries(pkg.peerDependencies)) {
744
- if (resolved.has(peerName) || seen.has(peerName))
745
- continue;
746
803
  topLevelNames.add(peerName);
747
804
  if (inheritBestEffort)
748
805
  bestEffortNames.add(peerName);
749
- queue.push([peerName, peerRange]);
806
+ queue.push({ name: peerName, range: peerRange, from: actual, kind: 'peer' });
750
807
  }
751
808
  }
752
809
  // X.5-F R2.5 + X.5-J: optional peers when THIS pkg is the
@@ -757,11 +814,9 @@ export class NpmInstaller {
757
814
  const allPeers = pkg.__allPeerDependencies;
758
815
  if (allPeers) {
759
816
  for (const [peerName, peerRange] of Object.entries(allPeers)) {
760
- if (resolved.has(peerName) || seen.has(peerName))
761
- continue;
762
817
  topLevelNames.add(peerName);
763
818
  bestEffortNames.add(peerName);
764
- queue.push([peerName, peerRange]);
819
+ queue.push({ name: peerName, range: peerRange, from: actual, kind: 'peer' });
765
820
  }
766
821
  }
767
822
  }
@@ -776,13 +831,18 @@ export class NpmInstaller {
776
831
  // sees names in BFS order, so incremental propagation would freeze a
777
832
  // dev/optional first sighting before a later required edge lands.
778
833
  const requiredNames = new Set();
834
+ // Followed by name over every placement, root first.
835
+ const byName = new Map(resolved);
836
+ for (const pkg of nested.values())
837
+ if (!byName.has(pkg.name))
838
+ byName.set(pkg.name, pkg);
779
839
  const pendingRequired = Object.keys(specs).filter((name) => !optionalRoots.has(name));
780
840
  for (let i = 0; i < pendingRequired.length; i++) {
781
841
  const name = pendingRequired[i];
782
842
  if (requiredNames.has(name))
783
843
  continue;
784
844
  requiredNames.add(name);
785
- const pkg = resolved.get(name);
845
+ const pkg = byName.get(name);
786
846
  if (!pkg)
787
847
  continue;
788
848
  const optional = pkg.optionalDependencies ?? {};
@@ -829,7 +889,7 @@ export class NpmInstaller {
829
889
  `, dispatch barriers=${dispatchBarriers}, ` +
830
890
  `elapsed=${((Date.now() - t0) / 1000).toFixed(1)}s` +
831
891
  (unresolved.size > 0 ? `, unresolved=${unresolved.size}` : ''));
832
- return { resolved, unresolved, rejected };
892
+ return { resolved, nested, unresolved, rejected };
833
893
  }
834
894
  /**
835
895
  * Batch install via two-tier fan-out (Fanout).
@@ -855,31 +915,26 @@ export class NpmInstaller {
855
915
  * Two-tier topology re-expands the fan-out without re-introducing
856
916
  * the V8 cap risk.
857
917
  */
858
- async fetchViaBatchFacet(toFetch, hoistPlan, nmDir, pid) {
918
+ async fetchViaBatchFacet(toFetch, nmDir, pid) {
859
919
  const log = (msg) => this.onProgress?.(msg);
860
920
  const installed = [];
861
921
  const failed = [];
862
922
  let filesWritten = 0;
863
923
  const mtime = Date.now();
864
924
  // Every entry in `toFetch` has a tarball URL — the diff phase fails
865
- // the ones that don't rather than dropping them.
925
+ // the ones that don't rather than dropping them. `pkgDir` is the
926
+ // spec's identity: one version may land at two placements.
866
927
  const specs = toFetch
867
- .map((p) => ({
928
+ .map(({ placement, pkg: p }) => ({
868
929
  name: p.name,
869
930
  version: p.version,
870
931
  tarballUrl: p.tarballUrl,
871
932
  integrity: p.integrity || '',
872
- pkgDir: nmDir + '/' + p.name,
933
+ pkgDir: nmDir + '/' + placement,
873
934
  installRoot: nmDir,
874
935
  mtime,
875
936
  chunkSize: CHUNK_SIZE,
876
937
  }));
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
938
  if (specs.length === 0) {
884
939
  return { installed, failed, filesWritten };
885
940
  }
@@ -899,13 +954,22 @@ export class NpmInstaller {
899
954
  // the two made the phase width control install parallelism, so widening it
900
955
  // for latency widened the cold-start burst along with it.
901
956
  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
957
+ // A tarball's placements share a shard, which acquires it once.
958
+ const byTarball = new Map();
959
+ for (const spec of specs) {
960
+ const group = byTarball.get(spec.tarballUrl);
961
+ if (group)
962
+ group.push(spec);
963
+ else
964
+ byTarball.set(spec.tarballUrl, [spec]);
965
+ }
966
+ const shardCount = Math.min(byTarball.size, INSTALL_PEER_CAP);
967
+ // Round-robin assignment: tarball at idx → shard idx % shardCount.
968
+ // This produces ⌈tarballs / shardCount⌉ tarballs per shard at
905
969
  // most, with the imbalance bounded to ±1.
906
970
  const shards = Array.from({ length: shardCount }, () => []);
907
- specs.forEach((spec, idx) => {
908
- shards[idx % shardCount].push(spec);
971
+ [...byTarball.values()].forEach((group, idx) => {
972
+ shards[idx % shardCount].push(...group);
909
973
  });
910
974
  const nonEmptyShards = shards.filter((s) => s.length > 0);
911
975
  const topology = nonEmptyShards.length < IN_DO_THRESHOLD ? 'in-do (in-DO fanout)' : 'peer-do (peer-DO fanout)';
@@ -967,11 +1031,13 @@ export class NpmInstaller {
967
1031
  let okCount = 0;
968
1032
  let failCount = 0;
969
1033
  const reported = new Set();
970
- const specByPackage = new Map(specs.map((s) => [`${s.name}@${s.version}`, s]));
1034
+ const specByDir = new Map(specs.map((s) => [s.pkgDir, s]));
971
1035
  for (const r of result.perPackage) {
972
- reported.add(`${r.name}@${r.version}`);
1036
+ // A pre-placement shard reports no pkgDir; it only wrote root.
1037
+ const dir = r.pkgDir ?? `${nmDir}/${r.name}`;
1038
+ reported.add(dir);
973
1039
  // One `npm http` line per tarball, reporting the tier that served it.
974
- const spec = specByPackage.get(`${r.name}@${r.version}`);
1040
+ const spec = specByDir.get(dir);
975
1041
  if (spec && r.tarballSource) {
976
1042
  this.npmLog('http', r.tarballSource === 'registry'
977
1043
  ? npmHttpFetchLine(spec.tarballUrl, r.tarballElapsedMs ?? 0)
@@ -996,12 +1062,11 @@ export class NpmInstaller {
996
1062
  // returns short would otherwise leave its packages in neither list,
997
1063
  // which is the same silent partial one layer down.
998
1064
  for (const s of specs) {
999
- const id = `${s.name}@${s.version}`;
1000
- if (reported.has(id))
1065
+ if (reported.has(s.pkgDir))
1001
1066
  continue;
1002
- failed.push(id);
1067
+ failed.push(`${s.name}@${s.version}`);
1003
1068
  failCount++;
1004
- log(` [warn] ${id}: install shard returned no result for this package`);
1069
+ log(` [warn] ${s.name}@${s.version}: install shard returned no result for this package (${s.pkgDir})`);
1005
1070
  }
1006
1071
  // Fold facet counters into the supervisor's diagnostic state.
1007
1072
  recordInstallFacetCounters(result.facetCounters);
@@ -1166,7 +1231,8 @@ export class NpmInstaller {
1166
1231
  // info comes from the registry cache — if the cache miss happens
1167
1232
  // too, we play it safe and invalidate (a fresh resolve repopulates
1168
1233
  // it and is always correct).
1169
- for (const [, entry] of lockfile) {
1234
+ // An edge is met by any placement Node's walk from the entry finds.
1235
+ for (const [placement, entry] of lockfile) {
1170
1236
  const cached = this.cache.getRegistryEntry(entry.name, entry.resolvedVer);
1171
1237
  if (!cached)
1172
1238
  return false;
@@ -1175,67 +1241,61 @@ export class NpmInstaller {
1175
1241
  for (const depName of Object.keys(deps)) {
1176
1242
  if (Object.hasOwn(optional, depName))
1177
1243
  continue;
1178
- if (!lockfile.has(depName))
1244
+ if (!visiblePlacements(placement, depName).some((p) => lockfile.has(p)))
1179
1245
  return false;
1180
1246
  }
1181
1247
  const peers = safeJsonParse(cached.peerDepsJson || '{}', {});
1182
1248
  for (const peerName of Object.keys(peers)) {
1183
- if (!lockfile.has(peerName))
1249
+ if (!visiblePlacements(placement, peerName).some((p) => lockfile.has(p)))
1184
1250
  return false;
1185
1251
  }
1186
1252
  }
1187
1253
  return true;
1188
1254
  }
1189
- /**
1190
- * Convert a lockfile back to resolved packages (for cache restore).
1191
- */
1255
+ /** Convert a lockfile back to resolved packages: root by name, nested by placement path. */
1192
1256
  lockfileToResolved(lockfile) {
1193
1257
  const resolved = new Map();
1194
- for (const [name, entry] of lockfile) {
1195
- // Reconstruct from registry cache
1258
+ const nested = new Map();
1259
+ for (const [placement, entry] of lockfile) {
1260
+ const name = entry.name;
1261
+ // Reconstruct from the registry cache; on a miss, a minimal entry.
1196
1262
  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
- });
1209
- }
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
- });
1223
- }
1263
+ (placement === name ? resolved : nested).set(placement, cached ? {
1264
+ name: cached.name,
1265
+ version: cached.version,
1266
+ tarballUrl: cached.tarballUrl,
1267
+ integrity: cached.integrity,
1268
+ dependencies: safeJsonParse(cached.depsJson, {}),
1269
+ exports: safeJsonParse(cached.exportsJson, null),
1270
+ main: cached.main,
1271
+ module: cached.moduleField,
1272
+ bin: safeJsonParse(cached.binJson, {}),
1273
+ } : {
1274
+ name,
1275
+ version: entry.resolvedVer,
1276
+ tarballUrl: '',
1277
+ integrity: entry.integrity,
1278
+ dependencies: safeJsonParse(entry.depsJson, {}),
1279
+ exports: null,
1280
+ main: '',
1281
+ module: '',
1282
+ bin: {},
1283
+ });
1224
1284
  }
1225
- return resolved;
1285
+ return { resolved, nested };
1226
1286
  }
1227
1287
  /**
1228
1288
  * Write lockfile to SQLite.
1229
1289
  */
1230
- writeLockfile(projDir, resolved, _hoistPlan, nmDir) {
1290
+ writeLockfile(projDir, hoistPlan, nmDir) {
1231
1291
  const entries = new Map();
1232
- for (const [name, pkg] of resolved) {
1233
- entries.set(name, {
1234
- name,
1292
+ for (const { placement, pkg } of hoistPlacements(hoistPlan)) {
1293
+ entries.set(placement, {
1294
+ name: pkg.name,
1235
1295
  resolvedVer: pkg.version,
1236
1296
  integrity: pkg.integrity,
1237
1297
  depsJson: JSON.stringify(pkg.dependencies),
1238
- hoistedPath: nmDir + '/' + name,
1298
+ hoistedPath: nmDir + '/' + placement,
1239
1299
  });
1240
1300
  }
1241
1301
  this.cache.writeLockfile(projDir, entries, this.ctx);
@@ -73,5 +73,7 @@ export interface NpxResolveResult {
73
73
  * Callers can use describeNpxSelfInvocation()/formatNpxHelp() before calling
74
74
  * this resolver.
75
75
  */
76
- export declare function resolveNpxBinary(installer: NpmInstaller, vfs: CredentialedVfs, cwd: string, rawArgs: string[], log: (msg: string) => void, pid?: number): Promise<NpxResolveResult>;
76
+ export declare function resolveNpxBinary(installer: NpmInstaller, vfs: CredentialedVfs, cwd: string, rawArgs: string[], log: (msg: string) => void, pid?: number,
77
+ /** The command's `NPM_REGISTRY`; the npx cache install reads from it too. */
78
+ registry?: string): Promise<NpxResolveResult>;
77
79
  //# sourceMappingURL=npx-install.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"npx-install.d.ts","sourceRoot":"","sources":["../../src/npm/npx-install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAA0B,KAAK,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAE5G;gFACgF;AAChF,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAoC/C,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;AA+BtE,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAE9E;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAElE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAG7D;AAED,wBAAgB,aAAa,IAAI,MAAM,CAWtC;AA6KD;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;0CACsC;IACtC,MAAM,CAAC,EAAE,YAAY,GAAG,WAAW,GAAG,eAAe,CAAC;CACvD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,YAAY,EACvB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EAAE,EACjB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAC1B,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CAmE3B"}
1
+ {"version":3,"file":"npx-install.d.ts","sourceRoot":"","sources":["../../src/npm/npx-install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAA0B,KAAK,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAE5G;gFACgF;AAChF,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAoC/C,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;AA+BtE,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAE9E;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAElE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAG7D;AAED,wBAAgB,aAAa,IAAI,MAAM,CAWtC;AA6KD;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;0CACsC;IACtC,MAAM,CAAC,EAAE,YAAY,GAAG,WAAW,GAAG,eAAe,CAAC;CACvD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,YAAY,EACvB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EAAE,EACjB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAC1B,GAAG,CAAC,EAAE,MAAM;AACZ,6EAA6E;AAC7E,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,gBAAgB,CAAC,CAoE3B"}
@@ -281,7 +281,9 @@ function ensureNpxCachePackageJson(vfs, pkgName, pkgRange) {
281
281
  * Callers can use describeNpxSelfInvocation()/formatNpxHelp() before calling
282
282
  * this resolver.
283
283
  */
284
- export async function resolveNpxBinary(installer, vfs, cwd, rawArgs, log, pid) {
284
+ export async function resolveNpxBinary(installer, vfs, cwd, rawArgs, log, pid,
285
+ /** The command's `NPM_REGISTRY`; the npx cache install reads from it too. */
286
+ registry) {
285
287
  const parsed = parseNpxArgs(rawArgs);
286
288
  if ('error' in parsed) {
287
289
  return { ok: false, error: parsed.error };
@@ -310,6 +312,7 @@ export async function resolveNpxBinary(installer, vfs, cwd, rawArgs, log, pid) {
310
312
  const result = await installer.install(NPX_CACHE_DIR, {
311
313
  packages: [installSpec],
312
314
  pid,
315
+ registry,
313
316
  });
314
317
  if ((result.failed?.length || 0) > 0) {
315
318
  // Partial install — some package failed to resolve. The fix is
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Placement paths: where a package lands, relative to the project's
3
+ * `node_modules` — `react` at root, `pkg-types/node_modules/confbox` nested
4
+ * under the dependent whose range root does not satisfy. Node walks
5
+ * `node_modules` upward from the importer, so a nested copy is found by its
6
+ * dependent (and what is beneath it) and by nobody else.
7
+ */
8
+ /** `name` under `parent`; `''` is the project itself. */
9
+ export declare function nestedPlacement(parent: string, name: string): string;
10
+ /** The containing placement, `''` at root. */
11
+ export declare function parentPlacement(placement: string): string;
12
+ /** The package name a placement holds. */
13
+ export declare function placementName(placement: string): string;
14
+ /** Placements Node's walk visits for `name` from inside `from`, nearest first, root last. */
15
+ export declare function visiblePlacements(from: string, name: string): string[];
16
+ //# sourceMappingURL=placement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../src/npm/placement.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,yDAAyD;AACzD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,8CAA8C;AAC9C,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,0CAA0C;AAC1C,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAStE"}