isolate-package 1.33.0 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{isolate-DyRD5Zd_.mjs → isolate-DI3eUTci.mjs} +862 -263
- package/dist/isolate-DI3eUTci.mjs.map +1 -0
- package/dist/isolate-bin.mjs +5 -6
- package/dist/isolate-bin.mjs.map +1 -1
- package/package.json +23 -19
- package/src/get-internal-package-names.test.ts +1 -1
- package/src/get-internal-package-names.ts +2 -2
- package/src/isolate-bin.ts +5 -5
- package/src/isolate.ts +22 -17
- package/src/lib/config.test.ts +1 -1
- package/src/lib/config.ts +3 -3
- package/src/lib/lockfile/helpers/bun-lockfile.ts +153 -0
- package/src/lib/lockfile/helpers/generate-bun-lockfile.test.ts +3 -3
- package/src/lib/lockfile/helpers/generate-bun-lockfile.ts +14 -146
- package/src/lib/lockfile/helpers/generate-npm-lockfile.integration.test.ts +1 -5
- package/src/lib/lockfile/helpers/generate-npm-lockfile.test.ts +311 -16
- package/src/lib/lockfile/helpers/generate-npm-lockfile.ts +193 -22
- package/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts +2 -2
- package/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +6 -6
- package/src/lib/lockfile/helpers/generate-yarn-lockfile.ts +5 -5
- package/src/lib/lockfile/process-lockfile.test.ts +2 -2
- package/src/lib/manifest/helpers/adapt-internal-package-manifests.test.ts +3 -3
- package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +2 -2
- package/src/lib/manifest/helpers/adapt-manifest-internal-deps.ts +1 -1
- package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.test.ts +4 -4
- package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts +7 -7
- package/src/lib/manifest/helpers/resolve-catalog-dependencies.test.ts +410 -0
- package/src/lib/manifest/helpers/resolve-catalog-dependencies.ts +115 -27
- package/src/lib/manifest/io.ts +6 -2
- package/src/lib/manifest/validate-manifest.ts +2 -2
- package/src/lib/output/get-build-output-dir.ts +1 -1
- package/src/lib/output/pack-dependencies.ts +1 -1
- package/src/lib/output/process-build-output-files.ts +6 -17
- package/src/lib/package-manager/helpers/infer-from-files.ts +5 -5
- package/src/lib/package-manager/helpers/infer-from-manifest.ts +7 -8
- package/src/lib/package-manager/index.ts +1 -1
- package/src/lib/package-manager/names.ts +8 -10
- package/src/lib/patches/collect-installed-names-bun.test.ts +154 -0
- package/src/lib/patches/collect-installed-names-bun.ts +87 -0
- package/src/lib/patches/collect-installed-names-pnpm.test.ts +316 -0
- package/src/lib/patches/collect-installed-names-pnpm.ts +365 -0
- package/src/lib/patches/copy-patches.test.ts +130 -13
- package/src/lib/patches/copy-patches.ts +47 -10
- package/src/lib/patches/write-isolate-pnpm-workspace.test.ts +83 -3
- package/src/lib/patches/write-isolate-pnpm-workspace.ts +4 -4
- package/src/lib/registry/collect-reachable-package-names.test.ts +1 -1
- package/src/lib/registry/create-packages-registry.ts +34 -31
- package/src/lib/registry/helpers/find-packages-globs.ts +23 -19
- package/src/lib/registry/list-internal-packages.test.ts +2 -2
- package/src/lib/types.ts +2 -2
- package/src/lib/utils/filter-patched-dependencies.test.ts +1 -1
- package/src/lib/utils/filter-patched-dependencies.ts +2 -2
- package/src/lib/utils/get-dirname.ts +1 -1
- package/src/lib/utils/index.ts +1 -1
- package/src/lib/utils/json.ts +12 -14
- package/src/lib/utils/pack.ts +32 -22
- package/src/lib/utils/reset-isolate-dir.test.ts +165 -0
- package/src/lib/utils/reset-isolate-dir.ts +147 -0
- package/src/lib/utils/unpack.test.ts +76 -0
- package/src/lib/utils/unpack.ts +16 -10
- package/src/lib/utils/wait-for-complete-file.test.ts +105 -0
- package/src/lib/utils/wait-for-complete-file.ts +44 -0
- package/src/lib/utils/yaml.ts +8 -9
- package/src/testing/setup.ts +1 -1
- package/dist/isolate-DyRD5Zd_.mjs.map +0 -1
|
@@ -405,12 +405,14 @@ describe("buildIsolatedLockfileJson", () => {
|
|
|
405
405
|
});
|
|
406
406
|
|
|
407
407
|
/**
|
|
408
|
-
*
|
|
409
|
-
* entry
|
|
410
|
-
*
|
|
411
|
-
*
|
|
408
|
+
* Reproduces https://github.com/0x80/isolate-package/issues/187. When the
|
|
409
|
+
* target's nested entry remaps onto the same path as a hoisted entry still
|
|
410
|
+
* needed by another reachable dependency, the target's nested version must
|
|
411
|
+
* win at the new root (the target becomes the isolate root) and the
|
|
412
|
+
* displaced hoisted entry must be re-nested under each consumer that
|
|
413
|
+
* originally resolved to it.
|
|
412
414
|
*/
|
|
413
|
-
it("
|
|
415
|
+
it("re-nests the displaced hoisted entry under each consumer that resolved to it", () => {
|
|
414
416
|
const srcData: Parameters<typeof buildIsolatedLockfileJson>[0]["srcData"] =
|
|
415
417
|
{
|
|
416
418
|
name: "root",
|
|
@@ -430,13 +432,13 @@ describe("buildIsolatedLockfileJson", () => {
|
|
|
430
432
|
dependencies: { semver: "^7" },
|
|
431
433
|
},
|
|
432
434
|
"node_modules/shared": { resolved: "packages/shared", link: true },
|
|
433
|
-
/** Hoisted v7 used by the internal dep. */
|
|
435
|
+
/** Hoisted v7 used by the internal dep "shared". */
|
|
434
436
|
"node_modules/semver": {
|
|
435
437
|
version: "7.7.4",
|
|
436
438
|
resolved: "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
|
437
439
|
integrity: "sha512-hoisted-v7",
|
|
438
440
|
},
|
|
439
|
-
/** Nested v6 used by the target —
|
|
441
|
+
/** Nested v6 used by the target — collides with the hoisted one. */
|
|
440
442
|
"packages/api/node_modules/semver": {
|
|
441
443
|
version: "6.3.1",
|
|
442
444
|
resolved: "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
|
@@ -457,15 +459,308 @@ describe("buildIsolatedLockfileJson", () => {
|
|
|
457
459
|
{ location: "packages/api/node_modules/semver", isLink: false },
|
|
458
460
|
];
|
|
459
461
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
462
|
+
const out = buildIsolatedLockfileJson({
|
|
463
|
+
srcData,
|
|
464
|
+
reachable,
|
|
465
|
+
targetImporterLoc: "packages/api",
|
|
466
|
+
targetLinkLoc: "node_modules/api",
|
|
467
|
+
targetPackageManifest: {
|
|
468
|
+
name: "api",
|
|
469
|
+
version: "1.0.0",
|
|
470
|
+
dependencies: { semver: "^6", shared: "file:./packages/shared" },
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
/** Target's nested v6 wins at the new root. */
|
|
475
|
+
expect(out.packages["node_modules/semver"]).toEqual({
|
|
476
|
+
version: "6.3.1",
|
|
477
|
+
resolved: "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
|
478
|
+
integrity: "sha512-nested-v6",
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
/** Original nested path must not leak through. */
|
|
482
|
+
expect(out.packages["packages/api/node_modules/semver"]).toBeUndefined();
|
|
483
|
+
|
|
484
|
+
/** Displaced v7 is re-nested under the consumer that resolved to it. */
|
|
485
|
+
expect(out.packages["packages/shared/node_modules/semver"]).toEqual({
|
|
486
|
+
version: "7.7.4",
|
|
487
|
+
resolved: "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
|
488
|
+
integrity: "sha512-hoisted-v7",
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* When multiple reachable consumers each resolve to the displaced hoisted
|
|
494
|
+
* entry, every consumer should get its own nested copy.
|
|
495
|
+
*/
|
|
496
|
+
it("re-nests the displaced entry under every consumer that needs it", () => {
|
|
497
|
+
const srcData: Parameters<typeof buildIsolatedLockfileJson>[0]["srcData"] =
|
|
498
|
+
{
|
|
499
|
+
name: "root",
|
|
500
|
+
version: "0.0.0",
|
|
501
|
+
lockfileVersion: 3,
|
|
502
|
+
requires: true,
|
|
503
|
+
packages: {
|
|
504
|
+
"": { name: "root", version: "0.0.0" },
|
|
505
|
+
"packages/api": {
|
|
506
|
+
name: "api",
|
|
507
|
+
version: "1.0.0",
|
|
508
|
+
dependencies: { "resolve-from": "^5" },
|
|
509
|
+
},
|
|
510
|
+
/** Target's nested override — wins at the new root. */
|
|
511
|
+
"packages/api/node_modules/resolve-from": {
|
|
512
|
+
version: "5.0.0",
|
|
513
|
+
resolved:
|
|
514
|
+
"https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
|
|
515
|
+
integrity: "sha512-nested-v5",
|
|
516
|
+
},
|
|
517
|
+
/** Hoisted older version used by two transitive deps. */
|
|
518
|
+
"node_modules/resolve-from": {
|
|
519
|
+
version: "4.0.0",
|
|
520
|
+
resolved:
|
|
521
|
+
"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
|
522
|
+
integrity: "sha512-hoisted-v4",
|
|
523
|
+
},
|
|
524
|
+
"node_modules/cosmiconfig": {
|
|
525
|
+
version: "7.0.0",
|
|
526
|
+
resolved:
|
|
527
|
+
"https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz",
|
|
528
|
+
integrity: "sha512-cosmi",
|
|
529
|
+
dependencies: { "resolve-from": "^4" },
|
|
530
|
+
},
|
|
531
|
+
"node_modules/import-fresh": {
|
|
532
|
+
version: "3.3.0",
|
|
533
|
+
resolved:
|
|
534
|
+
"https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
|
535
|
+
integrity: "sha512-import-fresh",
|
|
536
|
+
dependencies: { "resolve-from": "^4" },
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const reachable: ReachableNode[] = [
|
|
542
|
+
{ location: "packages/api", isLink: false },
|
|
543
|
+
{ location: "packages/api/node_modules/resolve-from", isLink: false },
|
|
544
|
+
{ location: "node_modules/resolve-from", isLink: false },
|
|
545
|
+
{ location: "node_modules/cosmiconfig", isLink: false },
|
|
546
|
+
{ location: "node_modules/import-fresh", isLink: false },
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
const out = buildIsolatedLockfileJson({
|
|
550
|
+
srcData,
|
|
551
|
+
reachable,
|
|
552
|
+
targetImporterLoc: "packages/api",
|
|
553
|
+
targetLinkLoc: "node_modules/api",
|
|
554
|
+
targetPackageManifest: {
|
|
555
|
+
name: "api",
|
|
556
|
+
version: "1.0.0",
|
|
557
|
+
dependencies: { "resolve-from": "^5" },
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
expect(out.packages["node_modules/resolve-from"]!.version).toBe("5.0.0");
|
|
562
|
+
expect(
|
|
563
|
+
out.packages["node_modules/cosmiconfig/node_modules/resolve-from"],
|
|
564
|
+
).toEqual({
|
|
565
|
+
version: "4.0.0",
|
|
566
|
+
resolved:
|
|
567
|
+
"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
|
568
|
+
integrity: "sha512-hoisted-v4",
|
|
569
|
+
});
|
|
570
|
+
expect(
|
|
571
|
+
out.packages["node_modules/import-fresh/node_modules/resolve-from"],
|
|
572
|
+
).toEqual({
|
|
573
|
+
version: "4.0.0",
|
|
574
|
+
resolved:
|
|
575
|
+
"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
|
576
|
+
integrity: "sha512-hoisted-v4",
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* If a reachable consumer has its own nested copy that already satisfies
|
|
582
|
+
* the dep, it should not get an additional copy of the displaced entry.
|
|
583
|
+
*/
|
|
584
|
+
it("skips consumers that have their own nested resolution", () => {
|
|
585
|
+
const srcData: Parameters<typeof buildIsolatedLockfileJson>[0]["srcData"] =
|
|
586
|
+
{
|
|
587
|
+
name: "root",
|
|
588
|
+
version: "0.0.0",
|
|
589
|
+
lockfileVersion: 3,
|
|
590
|
+
requires: true,
|
|
591
|
+
packages: {
|
|
592
|
+
"": { name: "root", version: "0.0.0" },
|
|
593
|
+
"packages/api": {
|
|
594
|
+
name: "api",
|
|
595
|
+
version: "1.0.0",
|
|
596
|
+
dependencies: { "resolve-from": "^5" },
|
|
597
|
+
},
|
|
598
|
+
"packages/api/node_modules/resolve-from": {
|
|
599
|
+
version: "5.0.0",
|
|
600
|
+
resolved:
|
|
601
|
+
"https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
|
|
602
|
+
integrity: "sha512-nested-v5",
|
|
603
|
+
},
|
|
604
|
+
/** Displaced hoisted version. */
|
|
605
|
+
"node_modules/resolve-from": {
|
|
606
|
+
version: "4.0.0",
|
|
607
|
+
resolved:
|
|
608
|
+
"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
|
609
|
+
integrity: "sha512-hoisted-v4",
|
|
610
|
+
},
|
|
611
|
+
/** Consumer with its own nested override (v3) — should not be re-nested. */
|
|
612
|
+
"node_modules/legacy-dep": {
|
|
613
|
+
version: "1.0.0",
|
|
614
|
+
resolved:
|
|
615
|
+
"https://registry.npmjs.org/legacy-dep/-/legacy-dep-1.0.0.tgz",
|
|
616
|
+
integrity: "sha512-legacy-dep",
|
|
617
|
+
dependencies: { "resolve-from": "^3" },
|
|
618
|
+
},
|
|
619
|
+
"node_modules/legacy-dep/node_modules/resolve-from": {
|
|
620
|
+
version: "3.0.0",
|
|
621
|
+
resolved:
|
|
622
|
+
"https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
|
|
623
|
+
integrity: "sha512-legacy-v3",
|
|
624
|
+
},
|
|
625
|
+
/** Consumer without an own override — resolves to the displaced v4. */
|
|
626
|
+
"node_modules/cosmiconfig": {
|
|
627
|
+
version: "7.0.0",
|
|
628
|
+
resolved:
|
|
629
|
+
"https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz",
|
|
630
|
+
integrity: "sha512-cosmi",
|
|
631
|
+
dependencies: { "resolve-from": "^4" },
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const reachable: ReachableNode[] = [
|
|
637
|
+
{ location: "packages/api", isLink: false },
|
|
638
|
+
{ location: "packages/api/node_modules/resolve-from", isLink: false },
|
|
639
|
+
{ location: "node_modules/resolve-from", isLink: false },
|
|
640
|
+
{ location: "node_modules/legacy-dep", isLink: false },
|
|
641
|
+
{
|
|
642
|
+
location: "node_modules/legacy-dep/node_modules/resolve-from",
|
|
643
|
+
isLink: false,
|
|
644
|
+
},
|
|
645
|
+
{ location: "node_modules/cosmiconfig", isLink: false },
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
const out = buildIsolatedLockfileJson({
|
|
649
|
+
srcData,
|
|
650
|
+
reachable,
|
|
651
|
+
targetImporterLoc: "packages/api",
|
|
652
|
+
targetLinkLoc: "node_modules/api",
|
|
653
|
+
targetPackageManifest: {
|
|
654
|
+
name: "api",
|
|
655
|
+
version: "1.0.0",
|
|
656
|
+
dependencies: { "resolve-from": "^5" },
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
/** legacy-dep keeps its own nested v3 — no extra copy at this path. */
|
|
661
|
+
expect(
|
|
662
|
+
out.packages["node_modules/legacy-dep/node_modules/resolve-from"]!
|
|
663
|
+
.version,
|
|
664
|
+
).toBe("3.0.0");
|
|
665
|
+
|
|
666
|
+
/** cosmiconfig had no own override and gets the displaced v4 nested. */
|
|
667
|
+
expect(
|
|
668
|
+
out.packages["node_modules/cosmiconfig/node_modules/resolve-from"]!
|
|
669
|
+
.version,
|
|
670
|
+
).toBe("4.0.0");
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Cascade scenario: a reachable node is simultaneously (a) the loser of one
|
|
675
|
+
* collision (its slot is taken by a target-nested winner) and (b) a
|
|
676
|
+
* potential consumer of another displaced entry. Because the displaced
|
|
677
|
+
* consumer is not actually present in the isolate, the re-nesting loop must
|
|
678
|
+
* skip it — otherwise it would try to nest under a slot already held by an
|
|
679
|
+
* unrelated target-nested entry and throw a spurious secondary-collision
|
|
680
|
+
* error.
|
|
681
|
+
*/
|
|
682
|
+
it("skips re-nesting under a consumer whose own entry has been displaced", () => {
|
|
683
|
+
const srcData: Parameters<typeof buildIsolatedLockfileJson>[0]["srcData"] =
|
|
684
|
+
{
|
|
685
|
+
name: "root",
|
|
686
|
+
version: "0.0.0",
|
|
687
|
+
lockfileVersion: 3,
|
|
688
|
+
requires: true,
|
|
689
|
+
packages: {
|
|
690
|
+
"": { name: "root", version: "0.0.0" },
|
|
691
|
+
"packages/api": {
|
|
692
|
+
name: "api",
|
|
693
|
+
version: "1.0.0",
|
|
694
|
+
dependencies: { "dep-a": "*", "dep-x": "^5" },
|
|
695
|
+
},
|
|
696
|
+
/** Target's nested A@v2 displaces the hoisted A@v1. */
|
|
697
|
+
"packages/api/node_modules/dep-a": {
|
|
698
|
+
version: "2.0.0",
|
|
699
|
+
resolved: "https://registry.npmjs.org/dep-a/-/dep-a-2.0.0.tgz",
|
|
700
|
+
integrity: "sha512-a-v2",
|
|
701
|
+
dependencies: { "dep-x": "^3" },
|
|
702
|
+
},
|
|
703
|
+
/** A@v2 brings its own nested X@v3. */
|
|
704
|
+
"packages/api/node_modules/dep-a/node_modules/dep-x": {
|
|
705
|
+
version: "3.0.0",
|
|
706
|
+
resolved: "https://registry.npmjs.org/dep-x/-/dep-x-3.0.0.tgz",
|
|
707
|
+
integrity: "sha512-x-v3",
|
|
708
|
+
},
|
|
709
|
+
/** Target's nested X@v5 displaces the hoisted X@v1. */
|
|
710
|
+
"packages/api/node_modules/dep-x": {
|
|
711
|
+
version: "5.0.0",
|
|
712
|
+
resolved: "https://registry.npmjs.org/dep-x/-/dep-x-5.0.0.tgz",
|
|
713
|
+
integrity: "sha512-x-v5",
|
|
714
|
+
},
|
|
715
|
+
/** Hoisted A@v1, still reachable through another transitive path. */
|
|
716
|
+
"node_modules/dep-a": {
|
|
717
|
+
version: "1.0.0",
|
|
718
|
+
resolved: "https://registry.npmjs.org/dep-a/-/dep-a-1.0.0.tgz",
|
|
719
|
+
integrity: "sha512-a-v1",
|
|
720
|
+
dependencies: { "dep-x": "^1" },
|
|
721
|
+
},
|
|
722
|
+
/** Hoisted X@v1, which A@v1 originally resolved to. */
|
|
723
|
+
"node_modules/dep-x": {
|
|
724
|
+
version: "1.0.0",
|
|
725
|
+
resolved: "https://registry.npmjs.org/dep-x/-/dep-x-1.0.0.tgz",
|
|
726
|
+
integrity: "sha512-x-v1",
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const reachable: ReachableNode[] = [
|
|
732
|
+
{ location: "packages/api", isLink: false },
|
|
733
|
+
{ location: "packages/api/node_modules/dep-a", isLink: false },
|
|
734
|
+
{
|
|
735
|
+
location: "packages/api/node_modules/dep-a/node_modules/dep-x",
|
|
736
|
+
isLink: false,
|
|
737
|
+
},
|
|
738
|
+
{ location: "packages/api/node_modules/dep-x", isLink: false },
|
|
739
|
+
{ location: "node_modules/dep-a", isLink: false },
|
|
740
|
+
{ location: "node_modules/dep-x", isLink: false },
|
|
741
|
+
];
|
|
742
|
+
|
|
743
|
+
const out = buildIsolatedLockfileJson({
|
|
744
|
+
srcData,
|
|
745
|
+
reachable,
|
|
746
|
+
targetImporterLoc: "packages/api",
|
|
747
|
+
targetLinkLoc: "node_modules/api",
|
|
748
|
+
targetPackageManifest: {
|
|
749
|
+
name: "api",
|
|
750
|
+
version: "1.0.0",
|
|
751
|
+
dependencies: { "dep-a": "*", "dep-x": "^5" },
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
/** The displaced hoisted A@v1 leaves no consumer present in the isolate
|
|
756
|
+
* (its only reachable consumer was itself), so no re-nesting happens for
|
|
757
|
+
* dep-x under node_modules/dep-a — the existing X@v3 placed via remap
|
|
758
|
+
* stays untouched. */
|
|
759
|
+
expect(out.packages["node_modules/dep-a"]!.version).toBe("2.0.0");
|
|
760
|
+
expect(out.packages["node_modules/dep-x"]!.version).toBe("5.0.0");
|
|
761
|
+
expect(out.packages["node_modules/dep-a/node_modules/dep-x"]!.version).toBe(
|
|
762
|
+
"3.0.0",
|
|
763
|
+
);
|
|
469
764
|
});
|
|
470
765
|
|
|
471
766
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Arborist from "@npmcli/arborist";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { useLogger } from "
|
|
5
|
-
import type { PackageManifest, PackagesRegistry } from "
|
|
6
|
-
import { getErrorMessage } from "
|
|
4
|
+
import { useLogger } from "#/lib/logger";
|
|
5
|
+
import type { PackageManifest, PackagesRegistry } from "#/lib/types";
|
|
6
|
+
import { getErrorMessage } from "#/lib/utils";
|
|
7
7
|
import { loadNpmConfig } from "./load-npm-config";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -114,9 +114,9 @@ export async function generateNpmLockfile({
|
|
|
114
114
|
"Created lockfile at",
|
|
115
115
|
path.join(isolateDir, "package-lock.json"),
|
|
116
116
|
);
|
|
117
|
-
} catch (
|
|
118
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
119
|
-
throw
|
|
117
|
+
} catch (error) {
|
|
118
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
119
|
+
throw error;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -160,7 +160,7 @@ async function generateFromRootLockfile({
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
if (typeof targetImporterNode.location !== "string") {
|
|
163
|
-
throw new
|
|
163
|
+
throw new TypeError(
|
|
164
164
|
`Target workspace "${targetPackageName}" resolved to a node without a location`,
|
|
165
165
|
);
|
|
166
166
|
}
|
|
@@ -277,11 +277,31 @@ export function buildIsolatedLockfileJson({
|
|
|
277
277
|
|
|
278
278
|
const targetNestedNodeModulesPrefix = `${targetImporterLoc}/node_modules/`;
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
280
|
+
const remapToOutputLoc = (origLoc: string): string => {
|
|
281
|
+
if (origLoc === targetImporterLoc) return "";
|
|
282
|
+
if (origLoc.startsWith(targetNestedNodeModulesPrefix)) {
|
|
283
|
+
return origLoc.slice(targetImporterLoc.length + 1);
|
|
284
|
+
}
|
|
285
|
+
return origLoc;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const isTargetNested = (origLoc: string): boolean =>
|
|
289
|
+
origLoc === targetImporterLoc ||
|
|
290
|
+
origLoc.startsWith(targetNestedNodeModulesPrefix);
|
|
291
|
+
|
|
292
|
+
/** Track the source location each output entry came from, used to identify
|
|
293
|
+
* the displaced entry when a collision occurs. */
|
|
283
294
|
const origLocByNewLoc = new Map<string, string>();
|
|
284
295
|
|
|
296
|
+
/** Collisions where the target's nested entry displaces a hoisted entry that
|
|
297
|
+
* is still reachable from other deps. The displaced entry needs to be
|
|
298
|
+
* re-nested under each consumer that originally resolved to it. */
|
|
299
|
+
type Collision = {
|
|
300
|
+
loserOrigLoc: string;
|
|
301
|
+
loserEntry: LockfilePackageEntry;
|
|
302
|
+
};
|
|
303
|
+
const collisions: Collision[] = [];
|
|
304
|
+
|
|
285
305
|
for (const node of reachable) {
|
|
286
306
|
const origLoc = node.location;
|
|
287
307
|
|
|
@@ -299,14 +319,7 @@ export function buildIsolatedLockfileJson({
|
|
|
299
319
|
* `packages/app/lib/core`) are preserved verbatim because their disk
|
|
300
320
|
* location in the isolate is unchanged.
|
|
301
321
|
*/
|
|
302
|
-
|
|
303
|
-
if (origLoc === targetImporterLoc) {
|
|
304
|
-
newLoc = "";
|
|
305
|
-
} else if (origLoc.startsWith(targetNestedNodeModulesPrefix)) {
|
|
306
|
-
newLoc = origLoc.slice(targetImporterLoc.length + 1);
|
|
307
|
-
} else {
|
|
308
|
-
newLoc = origLoc;
|
|
309
|
-
}
|
|
322
|
+
const newLoc = remapToOutputLoc(origLoc);
|
|
310
323
|
|
|
311
324
|
const srcEntry = srcPackages[origLoc];
|
|
312
325
|
if (!srcEntry) {
|
|
@@ -318,9 +331,36 @@ export function buildIsolatedLockfileJson({
|
|
|
318
331
|
const existing = outPackages[newLoc];
|
|
319
332
|
if (existing && !entriesAreEquivalent(existing, srcEntry)) {
|
|
320
333
|
const previousOrigLoc = origLocByNewLoc.get(newLoc) ?? "<unknown>";
|
|
334
|
+
const incomingIsTargetNested = isTargetNested(origLoc);
|
|
335
|
+
const previousIsTargetNested = isTargetNested(previousOrigLoc);
|
|
336
|
+
|
|
337
|
+
if (incomingIsTargetNested && !previousIsTargetNested) {
|
|
338
|
+
/** The target-nested entry wins. The previously-stored hoisted entry
|
|
339
|
+
* is displaced and must be re-nested under its consumers. */
|
|
340
|
+
collisions.push({
|
|
341
|
+
loserOrigLoc: previousOrigLoc,
|
|
342
|
+
loserEntry: existing,
|
|
343
|
+
});
|
|
344
|
+
outPackages[newLoc] = { ...srcEntry };
|
|
345
|
+
origLocByNewLoc.set(newLoc, origLoc);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!incomingIsTargetNested && previousIsTargetNested) {
|
|
350
|
+
/** The previously-stored target-nested entry wins; the incoming
|
|
351
|
+
* hoisted entry is the loser. */
|
|
352
|
+
collisions.push({
|
|
353
|
+
loserOrigLoc: origLoc,
|
|
354
|
+
loserEntry: { ...srcEntry },
|
|
355
|
+
});
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Neither side is the target's nested version, or both are — we have
|
|
360
|
+
* no rule to pick a winner. Bail loudly. */
|
|
321
361
|
throw new Error(
|
|
322
|
-
`Path collision at "${newLoc}": source locations "${previousOrigLoc}" and "${origLoc}" both map there with conflicting entries
|
|
323
|
-
`
|
|
362
|
+
`Path collision at "${newLoc}": source locations "${previousOrigLoc}" and "${origLoc}" both map there with conflicting entries and no rule applies to pick a winner ` +
|
|
363
|
+
`(neither is a target-nested override, or both are). ` +
|
|
324
364
|
`Please report a reproduction at https://github.com/0x80/isolate-package/issues.`,
|
|
325
365
|
);
|
|
326
366
|
}
|
|
@@ -329,6 +369,70 @@ export function buildIsolatedLockfileJson({
|
|
|
329
369
|
origLocByNewLoc.set(newLoc, origLoc);
|
|
330
370
|
}
|
|
331
371
|
|
|
372
|
+
/** Re-nest each displaced entry under the reachable consumers that
|
|
373
|
+
* originally resolved to it via node_modules walk-up. */
|
|
374
|
+
for (const collision of collisions) {
|
|
375
|
+
const loserName = extractPackageNameFromLockfileLoc(collision.loserOrigLoc);
|
|
376
|
+
if (!loserName) continue;
|
|
377
|
+
|
|
378
|
+
for (const consumer of reachable) {
|
|
379
|
+
const consumerSrcLoc = consumer.location;
|
|
380
|
+
if (consumerSrcLoc === collision.loserOrigLoc) continue;
|
|
381
|
+
if (consumerSrcLoc === targetLinkLoc) continue;
|
|
382
|
+
|
|
383
|
+
const consumerEntry = srcPackages[consumerSrcLoc];
|
|
384
|
+
if (!consumerEntry) continue;
|
|
385
|
+
/** Workspace links carry dependency metadata on the importer entry,
|
|
386
|
+
* not the link entry itself. Skip the link side. */
|
|
387
|
+
if (consumerEntry.link) continue;
|
|
388
|
+
|
|
389
|
+
if (!entryDependsOn(consumerEntry, loserName)) continue;
|
|
390
|
+
|
|
391
|
+
const resolvedSrcLoc = resolveDepInSrcLockfile(
|
|
392
|
+
consumerSrcLoc,
|
|
393
|
+
loserName,
|
|
394
|
+
srcPackages,
|
|
395
|
+
);
|
|
396
|
+
if (resolvedSrcLoc !== collision.loserOrigLoc) continue;
|
|
397
|
+
|
|
398
|
+
const consumerNewLoc = remapToOutputLoc(consumerSrcLoc);
|
|
399
|
+
/** Consumer maps to the isolate root (the target itself). The root
|
|
400
|
+
* slot is already taken by the winning version. The target's own
|
|
401
|
+
* dependencies use that version — we cannot serve a different one
|
|
402
|
+
* here without nesting under the target, which would be its own
|
|
403
|
+
* collision. Accept the resolution shift. */
|
|
404
|
+
if (consumerNewLoc === "") continue;
|
|
405
|
+
|
|
406
|
+
/** If the consumer was itself displaced by another collision (its
|
|
407
|
+
* src-side entry doesn't match the entry we actually placed at its
|
|
408
|
+
* new location), the consumer isn't really present in the isolate.
|
|
409
|
+
* Its original dep needs are irrelevant here. */
|
|
410
|
+
const consumerOutEntry = outPackages[consumerNewLoc];
|
|
411
|
+
if (
|
|
412
|
+
!consumerOutEntry ||
|
|
413
|
+
!entriesAreEquivalent(consumerOutEntry, consumerEntry)
|
|
414
|
+
) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const nestedLoc = `${consumerNewLoc}/node_modules/${loserName}`;
|
|
419
|
+
|
|
420
|
+
const existingNested = outPackages[nestedLoc];
|
|
421
|
+
if (existingNested) {
|
|
422
|
+
if (entriesAreEquivalent(existingNested, collision.loserEntry)) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
throw new Error(
|
|
426
|
+
`Cannot re-nest displaced "${loserName}" under "${consumerNewLoc}": ` +
|
|
427
|
+
`the slot "${nestedLoc}" already contains a different entry. ` +
|
|
428
|
+
`Please report a reproduction at https://github.com/0x80/isolate-package/issues.`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
outPackages[nestedLoc] = { ...collision.loserEntry };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
332
436
|
/**
|
|
333
437
|
* If the target importer didn't make it into the reachable set for any
|
|
334
438
|
* reason (upstream Arborist bug, programmer error), bail loudly rather
|
|
@@ -341,8 +445,10 @@ export function buildIsolatedLockfileJson({
|
|
|
341
445
|
}
|
|
342
446
|
|
|
343
447
|
/** Overlay the isolate root with the adapted target manifest. */
|
|
344
|
-
const rootEntry: LockfilePackageEntry = {
|
|
345
|
-
|
|
448
|
+
const rootEntry: LockfilePackageEntry = {
|
|
449
|
+
...outPackages[""],
|
|
450
|
+
name: targetPackageManifest.name,
|
|
451
|
+
};
|
|
346
452
|
if (targetPackageManifest.version) {
|
|
347
453
|
rootEntry.version = targetPackageManifest.version;
|
|
348
454
|
}
|
|
@@ -396,6 +502,71 @@ function entriesAreEquivalent(
|
|
|
396
502
|
);
|
|
397
503
|
}
|
|
398
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Extracts the package name from a lockfile install location. Handles scoped
|
|
507
|
+
* packages, where the name is two segments after the last `node_modules/`.
|
|
508
|
+
*
|
|
509
|
+
* "node_modules/foo" -> "foo"
|
|
510
|
+
* "node_modules/@scope/foo" -> "@scope/foo"
|
|
511
|
+
* "node_modules/a/node_modules/b" -> "b"
|
|
512
|
+
* "node_modules/a/node_modules/@scope/b" -> "@scope/b"
|
|
513
|
+
*
|
|
514
|
+
* Returns null for locations that don't contain `node_modules/`.
|
|
515
|
+
*/
|
|
516
|
+
function extractPackageNameFromLockfileLoc(loc: string): string | null {
|
|
517
|
+
const marker = "node_modules/";
|
|
518
|
+
const lastIdx = loc.lastIndexOf(marker);
|
|
519
|
+
if (lastIdx < 0) return null;
|
|
520
|
+
const tail = loc.slice(lastIdx + marker.length);
|
|
521
|
+
if (tail.startsWith("@")) {
|
|
522
|
+
const slashIdx = tail.indexOf("/");
|
|
523
|
+
if (slashIdx < 0) return tail;
|
|
524
|
+
/** Stop at the second `/` so we don't include any further nesting. */
|
|
525
|
+
const secondSlash = tail.indexOf("/", slashIdx + 1);
|
|
526
|
+
return secondSlash < 0 ? tail : tail.slice(0, secondSlash);
|
|
527
|
+
}
|
|
528
|
+
const slashIdx = tail.indexOf("/");
|
|
529
|
+
return slashIdx < 0 ? tail : tail.slice(0, slashIdx);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Returns true if the lockfile entry lists `depName` in any of its dependency
|
|
534
|
+
* fields. Includes peer/optional/dev because any of them may have been the
|
|
535
|
+
* reason for the dep being installed at runtime.
|
|
536
|
+
*/
|
|
537
|
+
function entryDependsOn(entry: LockfilePackageEntry, depName: string): boolean {
|
|
538
|
+
return (
|
|
539
|
+
entry.dependencies?.[depName] !== undefined ||
|
|
540
|
+
entry.devDependencies?.[depName] !== undefined ||
|
|
541
|
+
entry.peerDependencies?.[depName] !== undefined ||
|
|
542
|
+
entry.optionalDependencies?.[depName] !== undefined
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Resolves `depName` against `srcPackages` from the perspective of a consumer
|
|
548
|
+
* at `consumerLoc`, mirroring Node.js `node_modules` walk-up. Returns the
|
|
549
|
+
* lockfile key of the entry the consumer would load at runtime, or null when
|
|
550
|
+
* no candidate exists.
|
|
551
|
+
*/
|
|
552
|
+
function resolveDepInSrcLockfile(
|
|
553
|
+
consumerLoc: string,
|
|
554
|
+
depName: string,
|
|
555
|
+
srcPackages: Record<string, LockfilePackageEntry>,
|
|
556
|
+
): string | null {
|
|
557
|
+
let scope = consumerLoc;
|
|
558
|
+
while (true) {
|
|
559
|
+
const candidate =
|
|
560
|
+
scope === ""
|
|
561
|
+
? `node_modules/${depName}`
|
|
562
|
+
: `${scope}/node_modules/${depName}`;
|
|
563
|
+
if (srcPackages[candidate]) return candidate;
|
|
564
|
+
if (scope === "") return null;
|
|
565
|
+
const idx = scope.lastIndexOf("/node_modules/");
|
|
566
|
+
scope = idx < 0 ? "" : scope.slice(0, idx);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
399
570
|
function overlayManifestDeps(
|
|
400
571
|
entry: LockfilePackageEntry,
|
|
401
572
|
manifest: PackageManifest,
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
|
2
2
|
import { generatePnpmLockfile } from "./generate-pnpm-lockfile";
|
|
3
3
|
|
|
4
4
|
/** Mock utils */
|
|
5
|
-
vi.mock("
|
|
5
|
+
vi.mock("#/lib/utils", () => ({
|
|
6
6
|
getErrorMessage: vi.fn((err: Error) => err.message),
|
|
7
7
|
isRushWorkspace: vi.fn(() => false),
|
|
8
8
|
}));
|
|
@@ -57,7 +57,7 @@ const { pruneLockfile: pruneLockfile_v9 } = vi.mocked(
|
|
|
57
57
|
await import("pnpm_prune_lockfile_v9"),
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
-
const { isRushWorkspace } = vi.mocked(await import("
|
|
60
|
+
const { isRushWorkspace } = vi.mocked(await import("#/lib/utils"));
|
|
61
61
|
|
|
62
62
|
/** Reusable lockfile fixture */
|
|
63
63
|
function createMockLockfile() {
|
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
import { pruneLockfile as pruneLockfile_v8 } from "pnpm_prune_lockfile_v8";
|
|
14
14
|
import { pruneLockfile as pruneLockfile_v9 } from "pnpm_prune_lockfile_v9";
|
|
15
15
|
import { pick } from "remeda";
|
|
16
|
-
import { useLogger } from "
|
|
17
|
-
import type { PackageManifest, PackagesRegistry, PatchFile } from "
|
|
18
|
-
import { getErrorMessage, isRushWorkspace } from "
|
|
16
|
+
import { useLogger } from "#/lib/logger";
|
|
17
|
+
import type { PackageManifest, PackagesRegistry, PatchFile } from "#/lib/types";
|
|
18
|
+
import { getErrorMessage, isRushWorkspace } from "#/lib/utils";
|
|
19
19
|
import { pnpmMapImporter } from "./pnpm-map-importer";
|
|
20
20
|
|
|
21
21
|
export async function generatePnpmLockfile({
|
|
@@ -185,8 +185,8 @@ export async function generatePnpmLockfile({
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
log.debug("Created lockfile at", path.join(isolateDir, "pnpm-lock.yaml"));
|
|
188
|
-
} catch (
|
|
189
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
190
|
-
throw
|
|
188
|
+
} catch (error) {
|
|
189
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
190
|
+
throw error;
|
|
191
191
|
}
|
|
192
192
|
}
|