isolate-package 1.31.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.mjs +1 -1
- package/dist/{isolate-DtNAHzfa.mjs → isolate-BRD2AgVJ.mjs} +385 -123
- package/dist/isolate-BRD2AgVJ.mjs.map +1 -0
- package/dist/isolate-bin.mjs +1 -1
- package/package.json +1 -1
- package/src/isolate.ts +1 -0
- 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/dist/isolate-DtNAHzfa.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as detectPackageManager, i as defineConfig, l as readTypedJson, n as listInternalPackages, o as resolveConfig, r as createPackagesRegistry, s as resolveWorkspacePaths, t as isolate } from "./isolate-
|
|
1
|
+
import { c as detectPackageManager, i as defineConfig, l as readTypedJson, n as listInternalPackages, o as resolveConfig, r as createPackagesRegistry, s as resolveWorkspacePaths, t as isolate } from "./isolate-BRD2AgVJ.mjs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
//#region src/get-internal-package-names.ts
|
|
4
4
|
/**
|
|
@@ -75,10 +75,12 @@ function getPackageName(packageSpec) {
|
|
|
75
75
|
//#endregion
|
|
76
76
|
//#region src/lib/utils/filter-patched-dependencies.ts
|
|
77
77
|
/**
|
|
78
|
-
* Filters patched dependencies to only include patches for packages that
|
|
79
|
-
* 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.
|
|
80
82
|
*/
|
|
81
|
-
function filterPatchedDependencies({ patchedDependencies, targetPackageManifest, includeDevDependencies }) {
|
|
83
|
+
function filterPatchedDependencies({ patchedDependencies, targetPackageManifest, includeDevDependencies, reachableDependencyNames }) {
|
|
82
84
|
const log = useLogger();
|
|
83
85
|
if (!patchedDependencies || typeof patchedDependencies !== "object") return;
|
|
84
86
|
const filteredPatches = {};
|
|
@@ -86,27 +88,35 @@ function filterPatchedDependencies({ patchedDependencies, targetPackageManifest,
|
|
|
86
88
|
let excludedCount = 0;
|
|
87
89
|
for (const [packageSpec, patchInfo] of Object.entries(patchedDependencies)) {
|
|
88
90
|
const packageName = getPackageName(packageSpec);
|
|
89
|
-
/**
|
|
91
|
+
/** Direct production dependency */
|
|
90
92
|
if (targetPackageManifest.dependencies?.[packageName]) {
|
|
91
93
|
filteredPatches[packageSpec] = patchInfo;
|
|
92
94
|
includedCount++;
|
|
93
95
|
log.debug(`Including production dependency patch: ${packageSpec}`);
|
|
94
96
|
continue;
|
|
95
97
|
}
|
|
96
|
-
/**
|
|
97
|
-
if (targetPackageManifest.devDependencies?.[packageName]) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
log.debug(`Including dev dependency patch: ${packageSpec}`);
|
|
102
|
-
} else {
|
|
103
|
-
excludedCount++;
|
|
104
|
-
log.debug(`Excluding dev dependency patch: ${packageSpec}`);
|
|
105
|
-
}
|
|
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}`);
|
|
106
103
|
continue;
|
|
107
104
|
}
|
|
108
|
-
/**
|
|
109
|
-
|
|
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}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
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)`);
|
|
110
120
|
excludedCount++;
|
|
111
121
|
}
|
|
112
122
|
log.debug(`Filtered patches: ${includedCount} included, ${excludedCount} excluded`);
|
|
@@ -670,29 +680,227 @@ async function loadNpmConfig({ npmPath }) {
|
|
|
670
680
|
//#endregion
|
|
671
681
|
//#region src/lib/lockfile/helpers/generate-npm-lockfile.ts
|
|
672
682
|
/**
|
|
673
|
-
* Generate an isolated
|
|
674
|
-
*
|
|
675
|
-
*
|
|
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.
|
|
676
690
|
*/
|
|
677
|
-
async function generateNpmLockfile({ workspaceRootDir, isolateDir }) {
|
|
691
|
+
async function generateNpmLockfile({ workspaceRootDir, isolateDir, targetPackageName, targetPackageManifest, packagesRegistry, internalDepPackageNames }) {
|
|
678
692
|
const log = useLogger();
|
|
679
|
-
log.debug("Generating NPM lockfile...");
|
|
680
|
-
const nodeModulesPath = path.join(workspaceRootDir, "node_modules");
|
|
681
693
|
try {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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"));
|
|
691
713
|
} catch (err) {
|
|
692
714
|
log.error(`Failed to generate lockfile: ${getErrorMessage(err)}`);
|
|
693
715
|
throw err;
|
|
694
716
|
}
|
|
695
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
|
+
}
|
|
696
904
|
//#endregion
|
|
697
905
|
//#region src/lib/lockfile/helpers/pnpm-map-importer.ts
|
|
698
906
|
/** Convert dependency links */
|
|
@@ -830,24 +1038,26 @@ async function generateYarnLockfile({ workspaceRootDir, isolateDir }) {
|
|
|
830
1038
|
* be done is to remove the root dependencies and devDependencies, and rename
|
|
831
1039
|
* the path to the target package to act as the new root.
|
|
832
1040
|
*/
|
|
833
|
-
async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir, internalDepPackageNames, targetPackageDir, targetPackageManifest, patchedDependencies, config }) {
|
|
1041
|
+
async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir, internalDepPackageNames, targetPackageDir, targetPackageName, targetPackageManifest, patchedDependencies, config }) {
|
|
834
1042
|
const log = useLogger();
|
|
1043
|
+
const npmGeneratorParams = {
|
|
1044
|
+
workspaceRootDir,
|
|
1045
|
+
isolateDir,
|
|
1046
|
+
targetPackageName,
|
|
1047
|
+
targetPackageManifest,
|
|
1048
|
+
packagesRegistry,
|
|
1049
|
+
internalDepPackageNames
|
|
1050
|
+
};
|
|
835
1051
|
if (config.forceNpm) {
|
|
836
1052
|
log.debug("Forcing to use NPM for isolate output");
|
|
837
|
-
await generateNpmLockfile(
|
|
838
|
-
workspaceRootDir,
|
|
839
|
-
isolateDir
|
|
840
|
-
});
|
|
1053
|
+
await generateNpmLockfile(npmGeneratorParams);
|
|
841
1054
|
return true;
|
|
842
1055
|
}
|
|
843
1056
|
const { name, majorVersion } = usePackageManager();
|
|
844
1057
|
let usedFallbackToNpm = false;
|
|
845
1058
|
switch (name) {
|
|
846
1059
|
case "npm":
|
|
847
|
-
await generateNpmLockfile(
|
|
848
|
-
workspaceRootDir,
|
|
849
|
-
isolateDir
|
|
850
|
-
});
|
|
1060
|
+
await generateNpmLockfile(npmGeneratorParams);
|
|
851
1061
|
break;
|
|
852
1062
|
case "yarn":
|
|
853
1063
|
if (majorVersion === 1) await generateYarnLockfile({
|
|
@@ -856,10 +1066,7 @@ async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir,
|
|
|
856
1066
|
});
|
|
857
1067
|
else {
|
|
858
1068
|
log.warn("Detected modern version of Yarn. Using NPM lockfile fallback.");
|
|
859
|
-
await generateNpmLockfile(
|
|
860
|
-
workspaceRootDir,
|
|
861
|
-
isolateDir
|
|
862
|
-
});
|
|
1069
|
+
await generateNpmLockfile(npmGeneratorParams);
|
|
863
1070
|
usedFallbackToNpm = true;
|
|
864
1071
|
}
|
|
865
1072
|
break;
|
|
@@ -888,10 +1095,7 @@ async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir,
|
|
|
888
1095
|
break;
|
|
889
1096
|
default:
|
|
890
1097
|
log.warn(`Unexpected package manager ${name}. Using NPM for output`);
|
|
891
|
-
await generateNpmLockfile(
|
|
892
|
-
workspaceRootDir,
|
|
893
|
-
isolateDir
|
|
894
|
-
});
|
|
1098
|
+
await generateNpmLockfile(npmGeneratorParams);
|
|
895
1099
|
usedFallbackToNpm = true;
|
|
896
1100
|
}
|
|
897
1101
|
return usedFallbackToNpm;
|
|
@@ -1188,82 +1392,48 @@ async function unpackDependencies(packedFilesByName, packagesRegistry, tmpDir, i
|
|
|
1188
1392
|
}));
|
|
1189
1393
|
}
|
|
1190
1394
|
//#endregion
|
|
1191
|
-
//#region src/lib/
|
|
1192
|
-
async function copyPatches({ workspaceRootDir, targetPackageManifest, isolateDir, includeDevDependencies }) {
|
|
1193
|
-
const log = useLogger();
|
|
1194
|
-
const { name: packageManagerName } = usePackageManager();
|
|
1195
|
-
let patchedDependencies;
|
|
1196
|
-
/**
|
|
1197
|
-
* Only try reading pnpm-workspace.yaml for pnpm workspaces. Bun workspaces
|
|
1198
|
-
* don't have this file and the warning would be noisy.
|
|
1199
|
-
*/
|
|
1200
|
-
if (packageManagerName === "pnpm") try {
|
|
1201
|
-
patchedDependencies = readTypedYamlSync(path.join(workspaceRootDir, "pnpm-workspace.yaml"))?.patchedDependencies;
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
log.warn(`Could not read pnpm-workspace.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
1204
|
-
}
|
|
1205
|
-
if (!patchedDependencies || Object.keys(patchedDependencies).length === 0) {
|
|
1206
|
-
if (packageManagerName === "pnpm") log.debug("No patched dependencies found in pnpm-workspace.yaml; Falling back to workspace root package.json");
|
|
1207
|
-
else log.debug("Reading patched dependencies from workspace root package.json");
|
|
1208
|
-
try {
|
|
1209
|
-
const workspaceRootManifest = await readTypedJson(path.join(workspaceRootDir, "package.json"));
|
|
1210
|
-
/** PNPM stores patches under pnpm.patchedDependencies, Bun at the top level */
|
|
1211
|
-
patchedDependencies = workspaceRootManifest?.pnpm?.patchedDependencies ?? workspaceRootManifest?.patchedDependencies;
|
|
1212
|
-
} catch (error) {
|
|
1213
|
-
log.warn(`Could not read workspace root package.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
if (!patchedDependencies || Object.keys(patchedDependencies).length === 0) {
|
|
1217
|
-
log.debug("No patched dependencies found in workspace root package.json");
|
|
1218
|
-
return {};
|
|
1219
|
-
}
|
|
1220
|
-
log.debug(`Found ${Object.keys(patchedDependencies).length} patched dependencies in workspace`);
|
|
1221
|
-
const filteredPatches = filterPatchedDependencies({
|
|
1222
|
-
patchedDependencies,
|
|
1223
|
-
targetPackageManifest,
|
|
1224
|
-
includeDevDependencies
|
|
1225
|
-
});
|
|
1226
|
-
if (!filteredPatches) return {};
|
|
1227
|
-
/**
|
|
1228
|
-
* Read the pnpm lockfile to get patch hashes. Bun doesn't store hashes in
|
|
1229
|
-
* its lockfile so we skip this for Bun.
|
|
1230
|
-
*/
|
|
1231
|
-
const lockfilePatchedDependencies = packageManagerName === "pnpm" ? await readLockfilePatchedDependencies(workspaceRootDir) : void 0;
|
|
1232
|
-
const copiedPatches = {};
|
|
1233
|
-
for (const [packageSpec, patchPath] of Object.entries(filteredPatches)) {
|
|
1234
|
-
const sourcePatchPath = path.resolve(workspaceRootDir, patchPath);
|
|
1235
|
-
if (!fs.existsSync(sourcePatchPath)) {
|
|
1236
|
-
log.warn(`Patch file not found: ${getRootRelativeLogPath(sourcePatchPath, workspaceRootDir)}`);
|
|
1237
|
-
continue;
|
|
1238
|
-
}
|
|
1239
|
-
/** Preserve original folder structure */
|
|
1240
|
-
const targetPatchPath = path.join(isolateDir, patchPath);
|
|
1241
|
-
await fs.ensureDir(path.dirname(targetPatchPath));
|
|
1242
|
-
await fs.copy(sourcePatchPath, targetPatchPath);
|
|
1243
|
-
log.debug(`Copied patch for ${packageSpec}: ${patchPath}`);
|
|
1244
|
-
const hash = (lockfilePatchedDependencies?.[packageSpec])?.hash ?? "";
|
|
1245
|
-
if (packageManagerName === "pnpm" && !hash) log.warn(`No hash found for patch ${packageSpec} in lockfile`);
|
|
1246
|
-
copiedPatches[packageSpec] = {
|
|
1247
|
-
path: patchPath,
|
|
1248
|
-
hash
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
if (Object.keys(copiedPatches).length > 0) log.debug(`Copied ${Object.keys(copiedPatches).length} patch files`);
|
|
1252
|
-
return copiedPatches;
|
|
1253
|
-
}
|
|
1395
|
+
//#region src/lib/registry/collect-reachable-package-names.ts
|
|
1254
1396
|
/**
|
|
1255
|
-
*
|
|
1256
|
-
*
|
|
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.
|
|
1257
1416
|
*/
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
+
}
|
|
1267
1437
|
}
|
|
1268
1438
|
}
|
|
1269
1439
|
//#endregion
|
|
@@ -1383,6 +1553,97 @@ function listInternalPackages(manifest, packagesRegistry, { includeDevDependenci
|
|
|
1383
1553
|
return [...new Set(result)];
|
|
1384
1554
|
}
|
|
1385
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
|
|
1386
1647
|
//#region src/isolate.ts
|
|
1387
1648
|
const __dirname = getDirname(import.meta.url);
|
|
1388
1649
|
function createIsolator(config) {
|
|
@@ -1482,6 +1743,7 @@ function createIsolator(config) {
|
|
|
1482
1743
|
const copiedPatches = (packageManager.name === "pnpm" || packageManager.name === "bun") && !config.forceNpm ? await copyPatches({
|
|
1483
1744
|
workspaceRootDir,
|
|
1484
1745
|
targetPackageManifest: outputManifest,
|
|
1746
|
+
packagesRegistry,
|
|
1485
1747
|
isolateDir,
|
|
1486
1748
|
includeDevDependencies: config.includeDevDependencies
|
|
1487
1749
|
}) : {};
|
|
@@ -1575,4 +1837,4 @@ async function isolate(config) {
|
|
|
1575
1837
|
//#endregion
|
|
1576
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 };
|
|
1577
1839
|
|
|
1578
|
-
//# sourceMappingURL=isolate-
|
|
1840
|
+
//# sourceMappingURL=isolate-BRD2AgVJ.mjs.map
|