isolate-package 1.30.0 → 1.32.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.d.mts +7 -1
- package/dist/index.mjs +1 -1
- package/dist/{isolate-CJy3YyKG.mjs → isolate-BRD2AgVJ.mjs} +422 -125
- package/dist/isolate-BRD2AgVJ.mjs.map +1 -0
- package/dist/isolate-bin.mjs +3 -3
- package/dist/isolate-bin.mjs.map +1 -1
- package/package.json +11 -2
- package/src/isolate-bin.ts +2 -2
- package/src/isolate.ts +30 -0
- package/src/lib/config.ts +31 -6
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/package-lock.json +82 -0
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/package.json +8 -0
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/api/package.json +12 -0
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/shared/package.json +12 -0
- package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/utils/package.json +11 -0
- package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/package-lock.json +56 -0
- package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/package.json +8 -0
- package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/packages/api/package.json +11 -0
- package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/packages/other/package.json +11 -0
- package/src/lib/lockfile/helpers/generate-npm-lockfile.integration.test.ts +243 -0
- package/src/lib/lockfile/helpers/generate-npm-lockfile.test.ts +604 -0
- package/src/lib/lockfile/helpers/generate-npm-lockfile.ts +417 -21
- package/src/lib/lockfile/process-lockfile.test.ts +4 -0
- package/src/lib/lockfile/process-lockfile.ts +14 -16
- package/src/lib/patches/copy-patches.test.ts +78 -0
- package/src/lib/patches/copy-patches.ts +22 -1
- package/src/lib/registry/collect-reachable-package-names.test.ts +239 -0
- package/src/lib/registry/collect-reachable-package-names.ts +60 -0
- package/src/lib/registry/index.ts +1 -0
- package/src/lib/utils/filter-patched-dependencies.test.ts +77 -0
- package/src/lib/utils/filter-patched-dependencies.ts +41 -17
- package/src/lib/utils/is-rush-workspace.ts +6 -0
- package/dist/isolate-CJy3YyKG.mjs.map +0 -1
|
@@ -4,6 +4,7 @@ import assert from "node:assert";
|
|
|
4
4
|
import path, { join } from "node:path";
|
|
5
5
|
import { isEmpty, omit, pick, unique } from "remeda";
|
|
6
6
|
import { exec, execFileSync, execSync } from "node:child_process";
|
|
7
|
+
import { detectMonorepo } from "detect-monorepo";
|
|
7
8
|
import { pathToFileURL } from "node:url";
|
|
8
9
|
import { createConsola } from "consola";
|
|
9
10
|
import { fileURLToPath } from "url";
|
|
@@ -74,10 +75,12 @@ function getPackageName(packageSpec) {
|
|
|
74
75
|
//#endregion
|
|
75
76
|
//#region src/lib/utils/filter-patched-dependencies.ts
|
|
76
77
|
/**
|
|
77
|
-
* Filters patched dependencies to only include patches for packages that
|
|
78
|
-
* present in the
|
|
78
|
+
* Filters patched dependencies to only include patches for packages that will
|
|
79
|
+
* be present in the isolated output, either as a direct dependency of the
|
|
80
|
+
* target or as a transitive dependency reachable through internal workspace
|
|
81
|
+
* packages.
|
|
79
82
|
*/
|
|
80
|
-
function filterPatchedDependencies({ patchedDependencies, targetPackageManifest, includeDevDependencies }) {
|
|
83
|
+
function filterPatchedDependencies({ patchedDependencies, targetPackageManifest, includeDevDependencies, reachableDependencyNames }) {
|
|
81
84
|
const log = useLogger();
|
|
82
85
|
if (!patchedDependencies || typeof patchedDependencies !== "object") return;
|
|
83
86
|
const filteredPatches = {};
|
|
@@ -85,27 +88,35 @@ function filterPatchedDependencies({ patchedDependencies, targetPackageManifest,
|
|
|
85
88
|
let excludedCount = 0;
|
|
86
89
|
for (const [packageSpec, patchInfo] of Object.entries(patchedDependencies)) {
|
|
87
90
|
const packageName = getPackageName(packageSpec);
|
|
88
|
-
/**
|
|
91
|
+
/** Direct production dependency */
|
|
89
92
|
if (targetPackageManifest.dependencies?.[packageName]) {
|
|
90
93
|
filteredPatches[packageSpec] = patchInfo;
|
|
91
94
|
includedCount++;
|
|
92
95
|
log.debug(`Including production dependency patch: ${packageSpec}`);
|
|
93
96
|
continue;
|
|
94
97
|
}
|
|
95
|
-
/**
|
|
96
|
-
if (targetPackageManifest.devDependencies?.[packageName]) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
/** Direct dev dependency (respects the dev-deps flag) */
|
|
99
|
+
if (includeDevDependencies && targetPackageManifest.devDependencies?.[packageName]) {
|
|
100
|
+
filteredPatches[packageSpec] = patchInfo;
|
|
101
|
+
includedCount++;
|
|
102
|
+
log.debug(`Including dev dependency patch: ${packageSpec}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Reachable via an internal workspace package. This fires even when the
|
|
107
|
+
* package is also listed in the target's devDependencies with
|
|
108
|
+
* `includeDevDependencies=false`, because the package is still installed
|
|
109
|
+
* in the isolate as a prod transitive.
|
|
110
|
+
*/
|
|
111
|
+
if (reachableDependencyNames?.has(packageName)) {
|
|
112
|
+
filteredPatches[packageSpec] = patchInfo;
|
|
113
|
+
includedCount++;
|
|
114
|
+
log.debug(`Including transitive dependency patch: ${packageSpec}`);
|
|
105
115
|
continue;
|
|
106
116
|
}
|
|
107
|
-
/** Package
|
|
108
|
-
log.debug(`Excluding patch: ${packageSpec}
|
|
117
|
+
/** Package won't be installed in the isolate */
|
|
118
|
+
if (targetPackageManifest.devDependencies?.[packageName]) log.debug(`Excluding dev dependency patch: ${packageSpec}`);
|
|
119
|
+
else log.debug(`Excluding patch: ${packageSpec} (package "${packageName}" not reachable from target)`);
|
|
109
120
|
excludedCount++;
|
|
110
121
|
}
|
|
111
122
|
log.debug(`Filtered patches: ${includedCount} included, ${excludedCount} excluded`);
|
|
@@ -150,6 +161,12 @@ function inspectValue(value) {
|
|
|
150
161
|
/**
|
|
151
162
|
* Detect if this is a Rush monorepo. They use a very different structure so
|
|
152
163
|
* there are multiple places where we need to make exceptions based on this.
|
|
164
|
+
*
|
|
165
|
+
* This intentionally only checks the passed-in directory. Using the upward
|
|
166
|
+
* walk of `detectMonorepo` here would break callers that pass a subdirectory
|
|
167
|
+
* of the actual Rush root, because downstream code builds paths (like
|
|
168
|
+
* `common/config/rush`) and lockfile importer ids relative to the same
|
|
169
|
+
* directory it gets.
|
|
153
170
|
*/
|
|
154
171
|
function isRushWorkspace(workspaceRootDir) {
|
|
155
172
|
return fs$1.existsSync(path.join(workspaceRootDir, "rush.json"));
|
|
@@ -350,7 +367,7 @@ const configDefaults = {
|
|
|
350
367
|
targetPackagePath: void 0,
|
|
351
368
|
tsconfigPath: "./tsconfig.json",
|
|
352
369
|
workspacePackages: void 0,
|
|
353
|
-
workspaceRoot:
|
|
370
|
+
workspaceRoot: void 0,
|
|
354
371
|
forceNpm: false,
|
|
355
372
|
pickFromScripts: void 0,
|
|
356
373
|
omitFromScripts: void 0,
|
|
@@ -432,12 +449,25 @@ function validateConfig(config) {
|
|
|
432
449
|
* Resolve the target package directory and workspace root directory from the
|
|
433
450
|
* configuration. When targetPackagePath is set, the config is assumed to live
|
|
434
451
|
* at the workspace root. Otherwise it lives in the target package directory.
|
|
452
|
+
*
|
|
453
|
+
* When `workspaceRoot` is not explicitly set, auto-detect the monorepo root by
|
|
454
|
+
* walking upward from the target package directory.
|
|
435
455
|
*/
|
|
436
456
|
function resolveWorkspacePaths(config) {
|
|
437
457
|
const targetPackageDir = config.targetPackagePath ? path.join(process.cwd(), config.targetPackagePath) : process.cwd();
|
|
458
|
+
if (config.targetPackagePath) return {
|
|
459
|
+
targetPackageDir,
|
|
460
|
+
workspaceRootDir: process.cwd()
|
|
461
|
+
};
|
|
462
|
+
if (config.workspaceRoot !== void 0) return {
|
|
463
|
+
targetPackageDir,
|
|
464
|
+
workspaceRootDir: path.join(targetPackageDir, config.workspaceRoot)
|
|
465
|
+
};
|
|
466
|
+
const detected = detectMonorepo(targetPackageDir);
|
|
467
|
+
if (!detected) throw new Error(`Failed to auto-detect monorepo workspace root from ${targetPackageDir}. Set the 'workspaceRoot' config option explicitly.`);
|
|
438
468
|
return {
|
|
439
469
|
targetPackageDir,
|
|
440
|
-
workspaceRootDir:
|
|
470
|
+
workspaceRootDir: detected.rootDir
|
|
441
471
|
};
|
|
442
472
|
}
|
|
443
473
|
function resolveConfig(initialConfig) {
|
|
@@ -650,29 +680,227 @@ async function loadNpmConfig({ npmPath }) {
|
|
|
650
680
|
//#endregion
|
|
651
681
|
//#region src/lib/lockfile/helpers/generate-npm-lockfile.ts
|
|
652
682
|
/**
|
|
653
|
-
* Generate an isolated
|
|
654
|
-
*
|
|
655
|
-
*
|
|
683
|
+
* Generate an isolated NPM lockfile for the target package.
|
|
684
|
+
*
|
|
685
|
+
* When a root `package-lock.json` exists we preserve original resolved
|
|
686
|
+
* versions and integrity by copying entries verbatim from the source
|
|
687
|
+
* lockfile. When it doesn't (forceNpm from pnpm/bun/yarn or modern-Yarn
|
|
688
|
+
* fallback), we fall back to Arborist's `buildIdealTree` against the
|
|
689
|
+
* isolate directory, which matches the prior behaviour.
|
|
656
690
|
*/
|
|
657
|
-
async function generateNpmLockfile({ workspaceRootDir, isolateDir }) {
|
|
691
|
+
async function generateNpmLockfile({ workspaceRootDir, isolateDir, targetPackageName, targetPackageManifest, packagesRegistry, internalDepPackageNames }) {
|
|
658
692
|
const log = useLogger();
|
|
659
|
-
log.debug("Generating NPM lockfile...");
|
|
660
|
-
const nodeModulesPath = path.join(workspaceRootDir, "node_modules");
|
|
661
693
|
try {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
694
|
+
const rootLockfilePath = path.join(workspaceRootDir, "package-lock.json");
|
|
695
|
+
if (fs.existsSync(rootLockfilePath)) {
|
|
696
|
+
log.debug("Generating NPM lockfile from root package-lock.json...");
|
|
697
|
+
await generateFromRootLockfile({
|
|
698
|
+
workspaceRootDir,
|
|
699
|
+
isolateDir,
|
|
700
|
+
targetPackageName,
|
|
701
|
+
targetPackageManifest,
|
|
702
|
+
packagesRegistry,
|
|
703
|
+
internalDepPackageNames
|
|
704
|
+
});
|
|
705
|
+
} else {
|
|
706
|
+
log.debug("No root package-lock.json found; falling back to buildIdealTree generation");
|
|
707
|
+
await generateViaBuildIdealTree({
|
|
708
|
+
workspaceRootDir,
|
|
709
|
+
isolateDir
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
log.debug("Created lockfile at", path.join(isolateDir, "package-lock.json"));
|
|
671
713
|
} catch (err) {
|
|
672
714
|
log.error(`Failed to generate lockfile: ${getErrorMessage(err)}`);
|
|
673
715
|
throw err;
|
|
674
716
|
}
|
|
675
717
|
}
|
|
718
|
+
async function generateFromRootLockfile({ workspaceRootDir, isolateDir, targetPackageName, targetPackageManifest, packagesRegistry, internalDepPackageNames }) {
|
|
719
|
+
const log = useLogger();
|
|
720
|
+
const arborist = new Arborist({
|
|
721
|
+
path: workspaceRootDir,
|
|
722
|
+
...(await loadNpmConfig({ npmPath: workspaceRootDir })).flat
|
|
723
|
+
});
|
|
724
|
+
/**
|
|
725
|
+
* `loadVirtual` hydrates every Node with `resolved` and `integrity` taken
|
|
726
|
+
* directly from the lockfile entries. It performs no registry calls.
|
|
727
|
+
*/
|
|
728
|
+
const rootTree = await arborist.loadVirtual();
|
|
729
|
+
const targetImporterNode = arborist.workspaceNodes(rootTree, [targetPackageName])[0];
|
|
730
|
+
if (!targetImporterNode) throw new Error(`Target workspace "${targetPackageName}" not found in root package-lock.json`);
|
|
731
|
+
if (typeof targetImporterNode.location !== "string") throw new Error(`Target workspace "${targetPackageName}" resolved to a node without a location`);
|
|
732
|
+
/**
|
|
733
|
+
* `workspaceDependencySet` walks `edgesOut` from each seed node. It does
|
|
734
|
+
* not add the seed node itself to the result, so ensure the target
|
|
735
|
+
* importer is included.
|
|
736
|
+
*/
|
|
737
|
+
const reachableNodes = arborist.workspaceDependencySet(rootTree, [targetPackageName], false);
|
|
738
|
+
reachableNodes.add(targetImporterNode);
|
|
739
|
+
const srcData = rootTree.meta?.data;
|
|
740
|
+
if (!srcData || !srcData.packages || Object.keys(srcData.packages).length === 0) {
|
|
741
|
+
/**
|
|
742
|
+
* Arborist normalises v1 lockfiles to v3 in `loadVirtual`, but fall
|
|
743
|
+
* back defensively if the virtual tree still has no `packages` map
|
|
744
|
+
* (e.g. an unusual lockfile shape). The fallback generator reads
|
|
745
|
+
* node_modules and won't preserve original versions, but it will
|
|
746
|
+
* produce a valid lockfile rather than failing.
|
|
747
|
+
*/
|
|
748
|
+
useLogger().debug("Source lockfile has no `packages` map; falling back to buildIdealTree");
|
|
749
|
+
await generateViaBuildIdealTree({
|
|
750
|
+
workspaceRootDir,
|
|
751
|
+
isolateDir
|
|
752
|
+
});
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const reachable = [...reachableNodes].map((node) => ({
|
|
756
|
+
location: node.location,
|
|
757
|
+
isLink: node.isLink,
|
|
758
|
+
target: node.target ? { location: node.target.location } : void 0
|
|
759
|
+
}));
|
|
760
|
+
const internalDepLocs = /* @__PURE__ */ new Map();
|
|
761
|
+
for (const depName of internalDepPackageNames) {
|
|
762
|
+
const pkg = packagesRegistry[depName];
|
|
763
|
+
if (!pkg) throw new Error(`Package ${depName} not found in packages registry`);
|
|
764
|
+
internalDepLocs.set(depName, toPosix(pkg.rootRelativeDir));
|
|
765
|
+
}
|
|
766
|
+
const out = buildIsolatedLockfileJson({
|
|
767
|
+
srcData,
|
|
768
|
+
reachable,
|
|
769
|
+
targetImporterLoc: targetImporterNode.location,
|
|
770
|
+
targetLinkLoc: `node_modules/${targetPackageName}`,
|
|
771
|
+
targetPackageManifest
|
|
772
|
+
});
|
|
773
|
+
/**
|
|
774
|
+
* Overlay each internal dep's adapted manifest onto its lockfile entry
|
|
775
|
+
* so cross-internal-dep references use `file:` instead of `workspace:*`.
|
|
776
|
+
*/
|
|
777
|
+
for (const [, depLoc] of internalDepLocs) {
|
|
778
|
+
if (!out.packages[depLoc]) continue;
|
|
779
|
+
const adaptedManifestPath = path.join(isolateDir, depLoc, "package.json");
|
|
780
|
+
if (!fs.existsSync(adaptedManifestPath)) {
|
|
781
|
+
log.debug(`Adapted internal dep manifest missing at ${adaptedManifestPath}; leaving lockfile entry unchanged`);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const adapted = await fs.readJson(adaptedManifestPath);
|
|
785
|
+
overlayManifestDeps(out.packages[depLoc], adapted);
|
|
786
|
+
}
|
|
787
|
+
const outPath = path.join(isolateDir, "package-lock.json");
|
|
788
|
+
await fs.writeFile(outPath, JSON.stringify(out, null, 2) + "\n");
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Pure JSON rewrite of the source lockfile into an isolated lockfile.
|
|
792
|
+
* Extracted so it can be unit tested without mocking Arborist.
|
|
793
|
+
*/
|
|
794
|
+
function buildIsolatedLockfileJson({ srcData, reachable, targetImporterLoc, targetLinkLoc, targetPackageManifest }) {
|
|
795
|
+
const outPackages = {};
|
|
796
|
+
const srcPackages = srcData.packages;
|
|
797
|
+
if (!srcPackages[targetImporterLoc]) throw new Error(`Source lockfile has no entry for target importer "${targetImporterLoc}"`);
|
|
798
|
+
const targetNestedNodeModulesPrefix = `${targetImporterLoc}/node_modules/`;
|
|
799
|
+
/** Track the source location each output entry came from, so we can
|
|
800
|
+
* produce a clear error if two source paths remap to the same target.
|
|
801
|
+
*/
|
|
802
|
+
const origLocByNewLoc = /* @__PURE__ */ new Map();
|
|
803
|
+
for (const node of reachable) {
|
|
804
|
+
const origLoc = node.location;
|
|
805
|
+
/** The target's self-link has no place in the isolate (root IS the target). */
|
|
806
|
+
if (origLoc === targetLinkLoc) continue;
|
|
807
|
+
/**
|
|
808
|
+
* The target workspace becomes the isolate root, so:
|
|
809
|
+
* "packages/app" -> ""
|
|
810
|
+
* "packages/app/node_modules/<name>" -> "node_modules/<name>"
|
|
811
|
+
* "packages/app/node_modules/a/node_modules/b" -> "node_modules/a/node_modules/b"
|
|
812
|
+
*
|
|
813
|
+
* Only `node_modules` subpaths under the target are remapped — other
|
|
814
|
+
* paths (e.g. a nested workspace importer like
|
|
815
|
+
* `packages/app/lib/core`) are preserved verbatim because their disk
|
|
816
|
+
* location in the isolate is unchanged.
|
|
817
|
+
*/
|
|
818
|
+
let newLoc;
|
|
819
|
+
if (origLoc === targetImporterLoc) newLoc = "";
|
|
820
|
+
else if (origLoc.startsWith(targetNestedNodeModulesPrefix)) newLoc = origLoc.slice(targetImporterLoc.length + 1);
|
|
821
|
+
else newLoc = origLoc;
|
|
822
|
+
const srcEntry = srcPackages[origLoc];
|
|
823
|
+
if (!srcEntry) throw new Error(`Reachable node "${origLoc}" has no entry in source lockfile packages`);
|
|
824
|
+
const existing = outPackages[newLoc];
|
|
825
|
+
if (existing && !entriesAreEquivalent(existing, srcEntry)) {
|
|
826
|
+
const previousOrigLoc = origLocByNewLoc.get(newLoc) ?? "<unknown>";
|
|
827
|
+
throw new Error(`Path collision at "${newLoc}": source locations "${previousOrigLoc}" and "${origLoc}" both map there with conflicting entries. This happens when the target pins a nested version override that collides with a hoisted version still needed by another reachable dependency. Please report a reproduction at https://github.com/0x80/isolate-package/issues.`);
|
|
828
|
+
}
|
|
829
|
+
outPackages[newLoc] = { ...srcEntry };
|
|
830
|
+
origLocByNewLoc.set(newLoc, origLoc);
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* If the target importer didn't make it into the reachable set for any
|
|
834
|
+
* reason (upstream Arborist bug, programmer error), bail loudly rather
|
|
835
|
+
* than emit a synthesised root entry with no source metadata.
|
|
836
|
+
*/
|
|
837
|
+
if (!outPackages[""]) throw new Error(`Target importer "${targetImporterLoc}" was not present in the reachable node set; cannot construct isolate root entry`);
|
|
838
|
+
/** Overlay the isolate root with the adapted target manifest. */
|
|
839
|
+
const rootEntry = { ...outPackages[""] };
|
|
840
|
+
rootEntry.name = targetPackageManifest.name;
|
|
841
|
+
if (targetPackageManifest.version) rootEntry.version = targetPackageManifest.version;
|
|
842
|
+
overlayManifestDeps(rootEntry, targetPackageManifest);
|
|
843
|
+
/** The isolate is no longer a workspace root. */
|
|
844
|
+
delete rootEntry.workspaces;
|
|
845
|
+
outPackages[""] = rootEntry;
|
|
846
|
+
/**
|
|
847
|
+
* Spread unknown top-level fields from the source lockfile so future
|
|
848
|
+
* npm-introduced metadata survives isolation. Then override identity
|
|
849
|
+
* fields and the recomputed `packages`, and drop the legacy
|
|
850
|
+
* `dependencies` tree which would be stale now that `packages` has
|
|
851
|
+
* been subsetted.
|
|
852
|
+
*/
|
|
853
|
+
const out = {
|
|
854
|
+
...srcData,
|
|
855
|
+
name: targetPackageManifest.name,
|
|
856
|
+
version: targetPackageManifest.version,
|
|
857
|
+
lockfileVersion: srcData.lockfileVersion ?? 3,
|
|
858
|
+
packages: outPackages
|
|
859
|
+
};
|
|
860
|
+
/**
|
|
861
|
+
* `requires` is propagated via the `...srcData` spread when the source
|
|
862
|
+
* has it. Don't invent one when the source omitted it — that would be
|
|
863
|
+
* an unnecessary diff from the original lockfile shape.
|
|
864
|
+
*/
|
|
865
|
+
if (srcData.requires === void 0) delete out.requires;
|
|
866
|
+
delete out.dependencies;
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Two source entries that map to the same output location are only
|
|
871
|
+
* "equivalent" if they install identical content. We compare the fields
|
|
872
|
+
* that actually determine what npm fetches and stores — version, resolved
|
|
873
|
+
* URL, integrity, and the link flag for workspace links.
|
|
874
|
+
*/
|
|
875
|
+
function entriesAreEquivalent(a, b) {
|
|
876
|
+
return a.version === b.version && a.resolved === b.resolved && a.integrity === b.integrity && !!a.link === !!b.link;
|
|
877
|
+
}
|
|
878
|
+
function overlayManifestDeps(entry, manifest) {
|
|
879
|
+
for (const field of [
|
|
880
|
+
"dependencies",
|
|
881
|
+
"devDependencies",
|
|
882
|
+
"optionalDependencies",
|
|
883
|
+
"peerDependencies"
|
|
884
|
+
]) {
|
|
885
|
+
const value = manifest[field];
|
|
886
|
+
if (value) entry[field] = value;
|
|
887
|
+
else delete entry[field];
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function toPosix(p) {
|
|
891
|
+
return p.split(path.sep).join(path.posix.sep);
|
|
892
|
+
}
|
|
893
|
+
async function generateViaBuildIdealTree({ workspaceRootDir, isolateDir }) {
|
|
894
|
+
const nodeModulesPath = path.join(workspaceRootDir, "node_modules");
|
|
895
|
+
if (!fs.existsSync(nodeModulesPath)) throw new Error(`Failed to find node_modules at ${nodeModulesPath}`);
|
|
896
|
+
const { meta } = await new Arborist({
|
|
897
|
+
path: isolateDir,
|
|
898
|
+
...(await loadNpmConfig({ npmPath: workspaceRootDir })).flat
|
|
899
|
+
}).buildIdealTree();
|
|
900
|
+
meta?.commit();
|
|
901
|
+
const lockfilePath = path.join(isolateDir, "package-lock.json");
|
|
902
|
+
await fs.writeFile(lockfilePath, String(meta));
|
|
903
|
+
}
|
|
676
904
|
//#endregion
|
|
677
905
|
//#region src/lib/lockfile/helpers/pnpm-map-importer.ts
|
|
678
906
|
/** Convert dependency links */
|
|
@@ -810,24 +1038,26 @@ async function generateYarnLockfile({ workspaceRootDir, isolateDir }) {
|
|
|
810
1038
|
* be done is to remove the root dependencies and devDependencies, and rename
|
|
811
1039
|
* the path to the target package to act as the new root.
|
|
812
1040
|
*/
|
|
813
|
-
async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir, internalDepPackageNames, targetPackageDir, targetPackageManifest, patchedDependencies, config }) {
|
|
1041
|
+
async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir, internalDepPackageNames, targetPackageDir, targetPackageName, targetPackageManifest, patchedDependencies, config }) {
|
|
814
1042
|
const log = useLogger();
|
|
1043
|
+
const npmGeneratorParams = {
|
|
1044
|
+
workspaceRootDir,
|
|
1045
|
+
isolateDir,
|
|
1046
|
+
targetPackageName,
|
|
1047
|
+
targetPackageManifest,
|
|
1048
|
+
packagesRegistry,
|
|
1049
|
+
internalDepPackageNames
|
|
1050
|
+
};
|
|
815
1051
|
if (config.forceNpm) {
|
|
816
1052
|
log.debug("Forcing to use NPM for isolate output");
|
|
817
|
-
await generateNpmLockfile(
|
|
818
|
-
workspaceRootDir,
|
|
819
|
-
isolateDir
|
|
820
|
-
});
|
|
1053
|
+
await generateNpmLockfile(npmGeneratorParams);
|
|
821
1054
|
return true;
|
|
822
1055
|
}
|
|
823
1056
|
const { name, majorVersion } = usePackageManager();
|
|
824
1057
|
let usedFallbackToNpm = false;
|
|
825
1058
|
switch (name) {
|
|
826
1059
|
case "npm":
|
|
827
|
-
await generateNpmLockfile(
|
|
828
|
-
workspaceRootDir,
|
|
829
|
-
isolateDir
|
|
830
|
-
});
|
|
1060
|
+
await generateNpmLockfile(npmGeneratorParams);
|
|
831
1061
|
break;
|
|
832
1062
|
case "yarn":
|
|
833
1063
|
if (majorVersion === 1) await generateYarnLockfile({
|
|
@@ -836,10 +1066,7 @@ async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir,
|
|
|
836
1066
|
});
|
|
837
1067
|
else {
|
|
838
1068
|
log.warn("Detected modern version of Yarn. Using NPM lockfile fallback.");
|
|
839
|
-
await generateNpmLockfile(
|
|
840
|
-
workspaceRootDir,
|
|
841
|
-
isolateDir
|
|
842
|
-
});
|
|
1069
|
+
await generateNpmLockfile(npmGeneratorParams);
|
|
843
1070
|
usedFallbackToNpm = true;
|
|
844
1071
|
}
|
|
845
1072
|
break;
|
|
@@ -868,10 +1095,7 @@ async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir,
|
|
|
868
1095
|
break;
|
|
869
1096
|
default:
|
|
870
1097
|
log.warn(`Unexpected package manager ${name}. Using NPM for output`);
|
|
871
|
-
await generateNpmLockfile(
|
|
872
|
-
workspaceRootDir,
|
|
873
|
-
isolateDir
|
|
874
|
-
});
|
|
1098
|
+
await generateNpmLockfile(npmGeneratorParams);
|
|
875
1099
|
usedFallbackToNpm = true;
|
|
876
1100
|
}
|
|
877
1101
|
return usedFallbackToNpm;
|
|
@@ -1168,82 +1392,48 @@ async function unpackDependencies(packedFilesByName, packagesRegistry, tmpDir, i
|
|
|
1168
1392
|
}));
|
|
1169
1393
|
}
|
|
1170
1394
|
//#endregion
|
|
1171
|
-
//#region src/lib/
|
|
1172
|
-
async function copyPatches({ workspaceRootDir, targetPackageManifest, isolateDir, includeDevDependencies }) {
|
|
1173
|
-
const log = useLogger();
|
|
1174
|
-
const { name: packageManagerName } = usePackageManager();
|
|
1175
|
-
let patchedDependencies;
|
|
1176
|
-
/**
|
|
1177
|
-
* Only try reading pnpm-workspace.yaml for pnpm workspaces. Bun workspaces
|
|
1178
|
-
* don't have this file and the warning would be noisy.
|
|
1179
|
-
*/
|
|
1180
|
-
if (packageManagerName === "pnpm") try {
|
|
1181
|
-
patchedDependencies = readTypedYamlSync(path.join(workspaceRootDir, "pnpm-workspace.yaml"))?.patchedDependencies;
|
|
1182
|
-
} catch (error) {
|
|
1183
|
-
log.warn(`Could not read pnpm-workspace.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
1184
|
-
}
|
|
1185
|
-
if (!patchedDependencies || Object.keys(patchedDependencies).length === 0) {
|
|
1186
|
-
if (packageManagerName === "pnpm") log.debug("No patched dependencies found in pnpm-workspace.yaml; Falling back to workspace root package.json");
|
|
1187
|
-
else log.debug("Reading patched dependencies from workspace root package.json");
|
|
1188
|
-
try {
|
|
1189
|
-
const workspaceRootManifest = await readTypedJson(path.join(workspaceRootDir, "package.json"));
|
|
1190
|
-
/** PNPM stores patches under pnpm.patchedDependencies, Bun at the top level */
|
|
1191
|
-
patchedDependencies = workspaceRootManifest?.pnpm?.patchedDependencies ?? workspaceRootManifest?.patchedDependencies;
|
|
1192
|
-
} catch (error) {
|
|
1193
|
-
log.warn(`Could not read workspace root package.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
if (!patchedDependencies || Object.keys(patchedDependencies).length === 0) {
|
|
1197
|
-
log.debug("No patched dependencies found in workspace root package.json");
|
|
1198
|
-
return {};
|
|
1199
|
-
}
|
|
1200
|
-
log.debug(`Found ${Object.keys(patchedDependencies).length} patched dependencies in workspace`);
|
|
1201
|
-
const filteredPatches = filterPatchedDependencies({
|
|
1202
|
-
patchedDependencies,
|
|
1203
|
-
targetPackageManifest,
|
|
1204
|
-
includeDevDependencies
|
|
1205
|
-
});
|
|
1206
|
-
if (!filteredPatches) return {};
|
|
1207
|
-
/**
|
|
1208
|
-
* Read the pnpm lockfile to get patch hashes. Bun doesn't store hashes in
|
|
1209
|
-
* its lockfile so we skip this for Bun.
|
|
1210
|
-
*/
|
|
1211
|
-
const lockfilePatchedDependencies = packageManagerName === "pnpm" ? await readLockfilePatchedDependencies(workspaceRootDir) : void 0;
|
|
1212
|
-
const copiedPatches = {};
|
|
1213
|
-
for (const [packageSpec, patchPath] of Object.entries(filteredPatches)) {
|
|
1214
|
-
const sourcePatchPath = path.resolve(workspaceRootDir, patchPath);
|
|
1215
|
-
if (!fs.existsSync(sourcePatchPath)) {
|
|
1216
|
-
log.warn(`Patch file not found: ${getRootRelativeLogPath(sourcePatchPath, workspaceRootDir)}`);
|
|
1217
|
-
continue;
|
|
1218
|
-
}
|
|
1219
|
-
/** Preserve original folder structure */
|
|
1220
|
-
const targetPatchPath = path.join(isolateDir, patchPath);
|
|
1221
|
-
await fs.ensureDir(path.dirname(targetPatchPath));
|
|
1222
|
-
await fs.copy(sourcePatchPath, targetPatchPath);
|
|
1223
|
-
log.debug(`Copied patch for ${packageSpec}: ${patchPath}`);
|
|
1224
|
-
const hash = (lockfilePatchedDependencies?.[packageSpec])?.hash ?? "";
|
|
1225
|
-
if (packageManagerName === "pnpm" && !hash) log.warn(`No hash found for patch ${packageSpec} in lockfile`);
|
|
1226
|
-
copiedPatches[packageSpec] = {
|
|
1227
|
-
path: patchPath,
|
|
1228
|
-
hash
|
|
1229
|
-
};
|
|
1230
|
-
}
|
|
1231
|
-
if (Object.keys(copiedPatches).length > 0) log.debug(`Copied ${Object.keys(copiedPatches).length} patch files`);
|
|
1232
|
-
return copiedPatches;
|
|
1233
|
-
}
|
|
1395
|
+
//#region src/lib/registry/collect-reachable-package-names.ts
|
|
1234
1396
|
/**
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1397
|
+
* Walk the target manifest and the manifests of any internal (workspace)
|
|
1398
|
+
* packages reachable from it, collecting every dependency name encountered
|
|
1399
|
+
* (both internal and external).
|
|
1400
|
+
*
|
|
1401
|
+
* The resulting set is a superset of the target's direct dependencies: it also
|
|
1402
|
+
* includes dependencies of internal workspace packages that will end up in the
|
|
1403
|
+
* isolated output. This is used to filter workspace-level
|
|
1404
|
+
* `patchedDependencies` so that patches for deps introduced via internal
|
|
1405
|
+
* packages aren't dropped.
|
|
1406
|
+
*
|
|
1407
|
+
* `dependencies`, `optionalDependencies`, and `peerDependencies` are all
|
|
1408
|
+
* walked — any of them can lead to a package being installed in the isolate
|
|
1409
|
+
* (pnpm installs peers by default via `autoInstallPeers`). devDependencies of
|
|
1410
|
+
* internal packages are never followed, and devDependencies of the *target*
|
|
1411
|
+
* are followed only when `includeDevDependencies` is true.
|
|
1412
|
+
*
|
|
1413
|
+
* Note: only recurses through internal packages — manifests of external deps
|
|
1414
|
+
* aren't available here. Deep external→external transitives therefore won't
|
|
1415
|
+
* appear in the set.
|
|
1237
1416
|
*/
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1417
|
+
function collectReachablePackageNames({ targetPackageManifest, packagesRegistry, includeDevDependencies }) {
|
|
1418
|
+
const names = /* @__PURE__ */ new Set();
|
|
1419
|
+
const visitedInternal = /* @__PURE__ */ new Set();
|
|
1420
|
+
walk(targetPackageManifest, true);
|
|
1421
|
+
return names;
|
|
1422
|
+
function walk(manifest, isTarget) {
|
|
1423
|
+
const depNames = [
|
|
1424
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
1425
|
+
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
1426
|
+
...Object.keys(manifest.peerDependencies ?? {}),
|
|
1427
|
+
...isTarget && includeDevDependencies ? Object.keys(manifest.devDependencies ?? {}) : []
|
|
1428
|
+
];
|
|
1429
|
+
for (const name of depNames) {
|
|
1430
|
+
names.add(name);
|
|
1431
|
+
const internalPkg = packagesRegistry[name];
|
|
1432
|
+
if (internalPkg && !visitedInternal.has(name)) {
|
|
1433
|
+
visitedInternal.add(name);
|
|
1434
|
+
walk(internalPkg.manifest, false);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1247
1437
|
}
|
|
1248
1438
|
}
|
|
1249
1439
|
//#endregion
|
|
@@ -1363,6 +1553,97 @@ function listInternalPackages(manifest, packagesRegistry, { includeDevDependenci
|
|
|
1363
1553
|
return [...new Set(result)];
|
|
1364
1554
|
}
|
|
1365
1555
|
//#endregion
|
|
1556
|
+
//#region src/lib/patches/copy-patches.ts
|
|
1557
|
+
async function copyPatches({ workspaceRootDir, targetPackageManifest, packagesRegistry, isolateDir, includeDevDependencies }) {
|
|
1558
|
+
const log = useLogger();
|
|
1559
|
+
const { name: packageManagerName } = usePackageManager();
|
|
1560
|
+
let patchedDependencies;
|
|
1561
|
+
/**
|
|
1562
|
+
* Only try reading pnpm-workspace.yaml for pnpm workspaces. Bun workspaces
|
|
1563
|
+
* don't have this file and the warning would be noisy.
|
|
1564
|
+
*/
|
|
1565
|
+
if (packageManagerName === "pnpm") try {
|
|
1566
|
+
patchedDependencies = readTypedYamlSync(path.join(workspaceRootDir, "pnpm-workspace.yaml"))?.patchedDependencies;
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
log.warn(`Could not read pnpm-workspace.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
1569
|
+
}
|
|
1570
|
+
if (!patchedDependencies || Object.keys(patchedDependencies).length === 0) {
|
|
1571
|
+
if (packageManagerName === "pnpm") log.debug("No patched dependencies found in pnpm-workspace.yaml; Falling back to workspace root package.json");
|
|
1572
|
+
else log.debug("Reading patched dependencies from workspace root package.json");
|
|
1573
|
+
try {
|
|
1574
|
+
const workspaceRootManifest = await readTypedJson(path.join(workspaceRootDir, "package.json"));
|
|
1575
|
+
/** PNPM stores patches under pnpm.patchedDependencies, Bun at the top level */
|
|
1576
|
+
patchedDependencies = workspaceRootManifest?.pnpm?.patchedDependencies ?? workspaceRootManifest?.patchedDependencies;
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
log.warn(`Could not read workspace root package.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (!patchedDependencies || Object.keys(patchedDependencies).length === 0) {
|
|
1582
|
+
log.debug("No patched dependencies found in workspace root package.json");
|
|
1583
|
+
return {};
|
|
1584
|
+
}
|
|
1585
|
+
log.debug(`Found ${Object.keys(patchedDependencies).length} patched dependencies in workspace`);
|
|
1586
|
+
/**
|
|
1587
|
+
* Collect the set of dependency names reachable from the target (direct deps
|
|
1588
|
+
* plus deps introduced by internal workspace packages). Patches for names in
|
|
1589
|
+
* this set are preserved even when the target doesn't list them directly —
|
|
1590
|
+
* see issue #167.
|
|
1591
|
+
*/
|
|
1592
|
+
const reachableDependencyNames = collectReachablePackageNames({
|
|
1593
|
+
targetPackageManifest,
|
|
1594
|
+
packagesRegistry,
|
|
1595
|
+
includeDevDependencies
|
|
1596
|
+
});
|
|
1597
|
+
const filteredPatches = filterPatchedDependencies({
|
|
1598
|
+
patchedDependencies,
|
|
1599
|
+
targetPackageManifest,
|
|
1600
|
+
includeDevDependencies,
|
|
1601
|
+
reachableDependencyNames
|
|
1602
|
+
});
|
|
1603
|
+
if (!filteredPatches) return {};
|
|
1604
|
+
/**
|
|
1605
|
+
* Read the pnpm lockfile to get patch hashes. Bun doesn't store hashes in
|
|
1606
|
+
* its lockfile so we skip this for Bun.
|
|
1607
|
+
*/
|
|
1608
|
+
const lockfilePatchedDependencies = packageManagerName === "pnpm" ? await readLockfilePatchedDependencies(workspaceRootDir) : void 0;
|
|
1609
|
+
const copiedPatches = {};
|
|
1610
|
+
for (const [packageSpec, patchPath] of Object.entries(filteredPatches)) {
|
|
1611
|
+
const sourcePatchPath = path.resolve(workspaceRootDir, patchPath);
|
|
1612
|
+
if (!fs.existsSync(sourcePatchPath)) {
|
|
1613
|
+
log.warn(`Patch file not found: ${getRootRelativeLogPath(sourcePatchPath, workspaceRootDir)}`);
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
/** Preserve original folder structure */
|
|
1617
|
+
const targetPatchPath = path.join(isolateDir, patchPath);
|
|
1618
|
+
await fs.ensureDir(path.dirname(targetPatchPath));
|
|
1619
|
+
await fs.copy(sourcePatchPath, targetPatchPath);
|
|
1620
|
+
log.debug(`Copied patch for ${packageSpec}: ${patchPath}`);
|
|
1621
|
+
const hash = (lockfilePatchedDependencies?.[packageSpec])?.hash ?? "";
|
|
1622
|
+
if (packageManagerName === "pnpm" && !hash) log.warn(`No hash found for patch ${packageSpec} in lockfile`);
|
|
1623
|
+
copiedPatches[packageSpec] = {
|
|
1624
|
+
path: patchPath,
|
|
1625
|
+
hash
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
if (Object.keys(copiedPatches).length > 0) log.debug(`Copied ${Object.keys(copiedPatches).length} patch files`);
|
|
1629
|
+
return copiedPatches;
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Read the patchedDependencies from the original lockfile to get the hashes.
|
|
1633
|
+
* Since the file content is the same after copying, the hash remains valid.
|
|
1634
|
+
*/
|
|
1635
|
+
async function readLockfilePatchedDependencies(workspaceRootDir) {
|
|
1636
|
+
try {
|
|
1637
|
+
const { majorVersion } = usePackageManager();
|
|
1638
|
+
const useVersion9 = majorVersion >= 9;
|
|
1639
|
+
const lockfileDir = isRushWorkspace(workspaceRootDir) ? path.join(workspaceRootDir, "common/config/rush") : workspaceRootDir;
|
|
1640
|
+
return (useVersion9 ? await readWantedLockfile$1(lockfileDir, { ignoreIncompatible: false }) : await readWantedLockfile(lockfileDir, { ignoreIncompatible: false }))?.patchedDependencies;
|
|
1641
|
+
} catch {
|
|
1642
|
+
/** Package manager not detected or lockfile not readable */
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
//#endregion
|
|
1366
1647
|
//#region src/isolate.ts
|
|
1367
1648
|
const __dirname = getDirname(import.meta.url);
|
|
1368
1649
|
function createIsolator(config) {
|
|
@@ -1414,6 +1695,21 @@ function createIsolator(config) {
|
|
|
1414
1695
|
const isProductionDependency = productionInternalPackageNames.includes(packageName);
|
|
1415
1696
|
validateManifestMandatoryFields(packageDef.manifest, getRootRelativeLogPath(packageDef.absoluteDir, workspaceRootDir), isProductionDependency);
|
|
1416
1697
|
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Validate that workspace dev dependencies of all packages being packed
|
|
1700
|
+
* have a version field. Even when dev dependencies are not included in the
|
|
1701
|
+
* isolation output, pnpm pack resolves workspace:* specifiers and requires
|
|
1702
|
+
* the version field to be present.
|
|
1703
|
+
*/
|
|
1704
|
+
const validatedPackageNames = new Set(internalPackageNames);
|
|
1705
|
+
const manifestsToPack = [targetPackageManifest, ...internalPackageNames.map((name) => got(packagesRegistry, name).manifest)];
|
|
1706
|
+
for (const manifest of manifestsToPack) for (const depName of Object.keys(manifest.devDependencies ?? {})) {
|
|
1707
|
+
if (validatedPackageNames.has(depName)) continue;
|
|
1708
|
+
const packageDef = packagesRegistry[depName];
|
|
1709
|
+
if (!packageDef) continue;
|
|
1710
|
+
validateManifestMandatoryFields(packageDef.manifest, getRootRelativeLogPath(packageDef.absoluteDir, workspaceRootDir), false);
|
|
1711
|
+
validatedPackageNames.add(depName);
|
|
1712
|
+
}
|
|
1417
1713
|
await unpackDependencies(await packDependencies({
|
|
1418
1714
|
internalPackageNames,
|
|
1419
1715
|
packagesRegistry,
|
|
@@ -1447,6 +1743,7 @@ function createIsolator(config) {
|
|
|
1447
1743
|
const copiedPatches = (packageManager.name === "pnpm" || packageManager.name === "bun") && !config.forceNpm ? await copyPatches({
|
|
1448
1744
|
workspaceRootDir,
|
|
1449
1745
|
targetPackageManifest: outputManifest,
|
|
1746
|
+
packagesRegistry,
|
|
1450
1747
|
isolateDir,
|
|
1451
1748
|
includeDevDependencies: config.includeDevDependencies
|
|
1452
1749
|
}) : {};
|
|
@@ -1540,4 +1837,4 @@ async function isolate(config) {
|
|
|
1540
1837
|
//#endregion
|
|
1541
1838
|
export { loadConfigFromFile as a, detectPackageManager as c, defineConfig as i, readTypedJson as l, listInternalPackages as n, resolveConfig as o, createPackagesRegistry as r, resolveWorkspacePaths as s, isolate as t, filterObjectUndefined as u };
|
|
1542
1839
|
|
|
1543
|
-
//# sourceMappingURL=isolate-
|
|
1840
|
+
//# sourceMappingURL=isolate-BRD2AgVJ.mjs.map
|