isolate-package
Version:
Isolate monorepo packages to form a self-contained deployable unit
193 lines (169 loc) • 6.34 kB
text/typescript
import assert from "node:assert";
import path from "node:path";
import {
getLockfileImporterId as getLockfileImporterId_v8,
readWantedLockfile as readWantedLockfile_v8,
writeWantedLockfile as writeWantedLockfile_v8,
} from "pnpm_lockfile_file_v8";
import {
getLockfileImporterId as getLockfileImporterId_v9,
readWantedLockfile as readWantedLockfile_v9,
writeWantedLockfile as writeWantedLockfile_v9,
} from "pnpm_lockfile_file_v9";
import { pruneLockfile as pruneLockfile_v8 } from "pnpm_prune_lockfile_v8";
import { pruneLockfile as pruneLockfile_v9 } from "pnpm_prune_lockfile_v9";
import { pick } from "remeda";
import { useLogger } from "~/lib/logger";
import type { PackageManifest, PackagesRegistry, PatchFile } from "~/lib/types";
import { getErrorMessage, isRushWorkspace } from "~/lib/utils";
import { pnpmMapImporter } from "./pnpm-map-importer";
export async function generatePnpmLockfile({
workspaceRootDir,
targetPackageDir,
isolateDir,
internalDepPackageNames,
packagesRegistry,
targetPackageManifest,
majorVersion,
includeDevDependencies,
patchedDependencies,
}: {
workspaceRootDir: string;
targetPackageDir: string;
isolateDir: string;
internalDepPackageNames: string[];
packagesRegistry: PackagesRegistry;
targetPackageManifest: PackageManifest;
majorVersion: number;
includeDevDependencies: boolean;
/** Pre-computed patched dependencies with transformed paths from copyPatches */
patchedDependencies?: Record<string, PatchFile>;
}) {
/**
* For now we will assume that the lockfile format might not change in the
* versions after 9, because we might get lucky. If it does change, things
* would break either way.
*/
const useVersion9 = majorVersion >= 9;
const log = useLogger();
log.debug("Generating PNPM lockfile...");
try {
const isRush = isRushWorkspace(workspaceRootDir);
const lockfile = useVersion9
? await readWantedLockfile_v9(
isRush
? path.join(workspaceRootDir, "common/config/rush")
: workspaceRootDir,
{
ignoreIncompatible: false,
},
)
: await readWantedLockfile_v8(
isRush
? path.join(workspaceRootDir, "common/config/rush")
: workspaceRootDir,
{
ignoreIncompatible: false,
},
);
assert(lockfile, `No input lockfile found at ${workspaceRootDir}`);
const targetImporterId = useVersion9
? getLockfileImporterId_v9(workspaceRootDir, targetPackageDir)
: getLockfileImporterId_v8(workspaceRootDir, targetPackageDir);
const directoryByPackageName = Object.fromEntries(
internalDepPackageNames.map((name) => {
const pkg = packagesRegistry[name];
assert(pkg, `Package ${name} not found in packages registry`);
return [name, pkg.rootRelativeDir];
}),
);
const relevantImporterIds = [
targetImporterId,
/**
* The directory paths happen to correspond with what PNPM calls the
* importer ids in the context of a lockfile.
*/
...Object.values(directoryByPackageName),
/**
* Split the path by the OS separator and join it back with the POSIX
* separator.
*
* The importerIds are built from directory names, so Windows Git Bash
* environments will have double backslashes in their ids:
* "packages\common" vs. "packages/common". Without this split & join, any
* packages not on the top-level will have ill-formatted importerIds and
* their entries will be missing from the lockfile.importers list.
*/
].map((x) => x.split(path.sep).join(path.posix.sep));
log.debug("Relevant importer ids:", relevantImporterIds);
/**
* In a Rush workspace the original lockfile is not in the root, so the
* importerIds have to be prefixed with `../../`, but that's not how they
* should be stored in the isolated lockfile, so we use the prefixed ids
* only for parsing.
*/
const relevantImporterIdsWithPrefix = relevantImporterIds.map((x) =>
isRush ? `../../${x}` : x,
);
lockfile.importers = Object.fromEntries(
Object.entries(
pick(lockfile.importers, relevantImporterIdsWithPrefix),
).map(([prefixedImporterId, importer]) => {
const importerId = isRush
? prefixedImporterId.replace("../../", "")
: prefixedImporterId;
if (importerId === targetImporterId) {
log.debug("Setting target package importer on root");
return [
".",
pnpmMapImporter(".", importer, {
includeDevDependencies,
directoryByPackageName,
}),
];
}
log.debug("Setting internal package importer:", importerId);
return [
importerId,
pnpmMapImporter(importerId, importer, {
includeDevDependencies: false,
directoryByPackageName,
}),
];
}),
);
log.debug("Pruning the lockfile");
const prunedLockfile = useVersion9
? pruneLockfile_v9(lockfile, targetPackageManifest, ".")
: pruneLockfile_v8(lockfile, targetPackageManifest, ".");
/** Pruning seems to remove the overrides from the lockfile */
if (lockfile.overrides) {
prunedLockfile.overrides = lockfile.overrides;
}
/** Add packageExtensionsChecksum back to the pruned lockfile if present */
if (lockfile.packageExtensionsChecksum) {
prunedLockfile.packageExtensionsChecksum =
lockfile.packageExtensionsChecksum;
}
/**
* Use pre-computed patched dependencies with transformed paths. The paths
* are already adapted by copyPatches to match the isolated directory
* structure, preserving the original folder structure (not flattened).
*/
if (useVersion9) {
await writeWantedLockfile_v9(isolateDir, {
...prunedLockfile,
patchedDependencies,
});
} else {
await writeWantedLockfile_v8(isolateDir, {
...prunedLockfile,
patchedDependencies,
});
}
log.debug("Created lockfile at", path.join(isolateDir, "pnpm-lock.yaml"));
} catch (err) {
log.error(`Failed to generate lockfile: ${getErrorMessage(err)}`);
throw err;
}
}