Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"yaml": "^2.8.1"
},
"devDependencies": {
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
"@types/fs-extra": "^11.0.4",
"@types/node": "^24.4.0",
"@types/npmcli__config": "^6.0.3",
Expand Down
40 changes: 23 additions & 17 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions src/isolate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "./lib/output";
import { detectPackageManager, shouldUsePnpmPack } from "./lib/package-manager";
import { getVersion } from "./lib/package-manager/helpers/infer-from-files";
import { copyPatches } from "./lib/patches/copy-patches";
import { createPackagesRegistry, listInternalPackages } from "./lib/registry";
import type { PackageManifest } from "./lib/types";
import {
Expand Down Expand Up @@ -210,6 +211,28 @@ export function createIsolator(config?: IsolateConfig) {
config,
});

/** Copy patch files if includePatchedDependencies is enabled */
const copiedPatches = await copyPatches({
workspaceRootDir,
targetPackageManifest,
isolateDir,
includePatchedDependencies: config.includePatchedDependencies,
includeDevDependencies: config.includeDevDependencies,
});

/** Add copied patches to the isolated package.json */
if (Object.keys(copiedPatches).length > 0) {
const manifest = await readManifest(isolateDir);
if (!manifest.pnpm) {
manifest.pnpm = {};
}
manifest.pnpm.patchedDependencies = copiedPatches;
await writeManifest(isolateDir, manifest);
log.debug(
`Added ${Object.keys(copiedPatches).length} patches to isolated package.json`
);
}

if (usedFallbackToNpm) {
/**
* When we fall back to NPM, we set the manifest package manager to the
Expand Down
101 changes: 93 additions & 8 deletions src/lib/lockfile/helpers/generate-pnpm-lockfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,83 @@ import {
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 type { Logger } from "~/lib/logger";
import { useLogger } from "~/lib/logger";
import type { PackageManifest, PackagesRegistry } from "~/lib/types";
import { getErrorMessage, isRushWorkspace } from "~/lib/utils";
import { pnpmMapImporter } from "./pnpm-map-importer";

function filterPatchedDependencies(
originalPatchedDependencies: any,
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The any type should be replaced with a more specific type. Consider using Record<string, string> or defining a proper type for patchedDependencies based on the pnpm specification.

Copilot uses AI. Check for mistakes.
targetPackageManifest: PackageManifest,
includeDevDependencies: boolean,
log: Logger
): any {
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type any should be replaced with a more specific type such as Record<string, string> | undefined to improve type safety.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +27
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new filterPatchedDependencies function lacks test coverage. Consider adding tests to verify the filtering logic for production dependencies, dev dependencies, and packages not found in target dependencies.

Copilot uses AI. Check for mistakes.
if (
!originalPatchedDependencies ||
typeof originalPatchedDependencies !== "object"
) {
return undefined;
}

const getPackageName = (packageSpec: string): string => {
// Handle scoped packages: @scope/package@version -> @scope/package
if (packageSpec.startsWith("@")) {
const parts = packageSpec.split("@");
return `@${parts[1]}`;
}
// Handle regular packages: package@version -> package
return packageSpec.split("@")[0];
};

const filteredPatches: any = {};
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace any type with Record<string, string> or the appropriate type for patchedDependencies to improve type safety.

Copilot uses AI. Check for mistakes.
let includedCount = 0;
let excludedCount = 0;

for (const [packageSpec, patchInfo] of Object.entries(
originalPatchedDependencies
)) {
const packageName = getPackageName(packageSpec);

// Check if it's a regular dependency
if (targetPackageManifest.dependencies?.[packageName]) {
filteredPatches[packageSpec] = patchInfo;
includedCount++;
log.debug(
`Including production dependency patch in lockfile: ${packageSpec}`
);
continue;
}

// Check if it's a dev dependency and we should include dev dependencies
if (targetPackageManifest.devDependencies?.[packageName]) {
if (includeDevDependencies) {
filteredPatches[packageSpec] = patchInfo;
includedCount++;
log.debug(`Including dev dependency patch in lockfile: ${packageSpec}`);
} else {
excludedCount++;
log.debug(
`Excluding dev dependency patch from lockfile: ${packageSpec}`
);
}
continue;
}

// Package not found in dependencies or devDependencies
log.debug(
`Excluding patch from lockfile: ${packageSpec} (package "${packageName}" not found in target dependencies)`
);
excludedCount++;
}

log.debug(
`Filtered patched dependencies: ${includedCount} included, ${excludedCount} excluded`
);

return Object.keys(filteredPatches).length > 0 ? filteredPatches : undefined;
}

export async function generatePnpmLockfile({
workspaceRootDir,
targetPackageDir,
Expand All @@ -40,14 +112,22 @@ export async function generatePnpmLockfile({
includePatchedDependencies: boolean;
}) {
/**
* 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.
* PNPM 10+ uses the same lockfile format as version 9, but with
* lockfileVersion: '10.0' Since @pnpm/lockfile-file v10 packages don't exist
* yet, we use v9 packages for PNPM 10+. This should work because PNPM
* maintains backward compatibility, but we log a warning to alert users of
* potential edge cases.
*/
const useVersion9 = majorVersion >= 9;

const log = useLogger();

if (majorVersion >= 10) {
log.debug(
`Using PNPM v${majorVersion} with v9 lockfile packages - this should work but may have limitations`
);
}

log.debug("Generating PNPM lockfile...");

try {
Expand Down Expand Up @@ -163,13 +243,18 @@ export async function generatePnpmLockfile({
}

/**
* Don't know how to map the patched dependencies yet, so we just include
* them but I don't think it would work like this. The important thing for
* now is that they are omitted by default, because that is the most common
* use case.
* Filter patched dependencies to only include patches for packages that
* will actually be present in the isolated lockfile based on dependency
* type. We read patchedDependencies from workspace root, but filter based
* on target package dependencies.
*/
const patchedDependencies = includePatchedDependencies
? lockfile.patchedDependencies
? filterPatchedDependencies(
lockfile.patchedDependencies,
targetPackageManifest,
includeDevDependencies,
log
)
: undefined;

useVersion9
Expand Down
Loading