Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c31efc2
Copy PNPM patches to isolated directory
0x80 Nov 27, 2025
a9e8943
Merge branch 'main' into thijs/1127-copy-patches-pnpm
0x80 Nov 27, 2025
1343678
1.27.0-0
0x80 Nov 27, 2025
3dd34f0
Use jsdoc comments consistently
0x80 Nov 27, 2025
1f8a8c2
Extract utility
0x80 Nov 27, 2025
979705d
Extract filtering logic
0x80 Nov 27, 2025
f704afe
Avoid patch naming collisions
0x80 Nov 27, 2025
0265999
Add tests for filter patched deps
0x80 Nov 27, 2025
b2ff870
Something
0x80 Nov 27, 2025
e72cae4
Use output manifest consistently
0x80 Nov 27, 2025
182a7cd
Add tests for copy patches
0x80 Nov 27, 2025
41a5251
Add tests for get package name
0x80 Nov 27, 2025
25fc4f2
Use correct log path function and fix formatting
0x80 Nov 27, 2025
f7896fe
Fix lockfile patch paths to match copied file locations
0x80 Nov 27, 2025
923da2e
1.27.0-1
0x80 Nov 27, 2025
52118ec
Format code
0x80 Nov 27, 2025
8b4702e
Rename misleading test for malformed scoped package
0x80 Nov 27, 2025
8ad26f5
Add explicit mocks for package manager and lockfile readers in tests
0x80 Nov 27, 2025
9b2beda
Refactor filterPatchedDependencies to use object parameter and intern…
0x80 Nov 27, 2025
6041e9b
Remove includePatchedDependencies config option
0x80 Nov 27, 2025
af08551
Simplify patch copying to preserve original folder structure
0x80 Nov 27, 2025
39a37aa
1.27.0-2
0x80 Nov 27, 2025
a94fde3
Only copy patches when output uses pnpm
0x80 Nov 27, 2025
c979b93
Format
0x80 Nov 27, 2025
ec48d5e
Update src/lib/lockfile/helpers/generate-pnpm-lockfile.ts
0x80 Nov 28, 2025
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
5 changes: 5 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,8 @@ The codebase uses `~/` as path alias for `src/` (configured in tsconfig.json).
## Testing

Tests use Vitest and are co-located with source files (`*.test.ts`).

## Code Style

- Use JSDoc style comments (`/** ... */`) for all comments, including
single-line comments
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "isolate-package",
"version": "1.26.1",
"version": "1.27.0-1",
"description": "Isolate a monorepo package with its shared dependencies to form a self-contained directory, compatible with Firebase deploy",
"author": "Thijs Koerselman",
"license": "MIT",
Expand Down
55 changes: 47 additions & 8 deletions src/isolate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 @@ -199,6 +200,18 @@ export function createIsolator(config?: IsolateConfig) {

await writeManifest(isolateDir, outputManifest);

/**
* Copy patch files before generating lockfile so the lockfile contains the
* correct transformed paths (flattened to patches/ with collision
* avoidance).
*/
const copiedPatches = await copyPatches({
workspaceRootDir,
targetPackageManifest: outputManifest,
isolateDir,
includeDevDependencies: config.includeDevDependencies,
});

/** Generate an isolated lockfile based on the original one */
const usedFallbackToNpm = await processLockfile({
workspaceRootDir,
Expand All @@ -208,18 +221,44 @@ export function createIsolator(config?: IsolateConfig) {
targetPackageDir,
targetPackageName: targetPackageManifest.name,
targetPackageManifest: outputManifest,
patchedDependencies:
Object.keys(copiedPatches).length > 0 ? copiedPatches : undefined,
config,
});

if (usedFallbackToNpm) {
/**
* When we fall back to NPM, we set the manifest package manager to the
* available NPM version.
*/
const hasCopiedPatches = Object.keys(copiedPatches).length > 0;

/** Update manifest if patches were copied or npm fallback is needed */
if (hasCopiedPatches || usedFallbackToNpm) {
const manifest = await readManifest(isolateDir);

const npmVersion = getVersion("npm");
manifest.packageManager = `npm@${npmVersion}`;
if (hasCopiedPatches) {
if (!manifest.pnpm) {
manifest.pnpm = {};
}
/**
* Extract just the paths for the manifest (lockfile needs full
* PatchFile)
*/
manifest.pnpm.patchedDependencies = Object.fromEntries(
Object.entries(copiedPatches).map(([spec, patchFile]) => [
spec,
patchFile.path,
])
);
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
* available NPM version.
*/
const npmVersion = getVersion("npm");
manifest.packageManager = `npm@${npmVersion}`;
}

await writeManifest(isolateDir, manifest);
}
Expand Down Expand Up @@ -286,7 +325,7 @@ export function createIsolator(config?: IsolateConfig) {
};
}

// Keep the original function for backward compatibility
/** Keep the original function for backward compatibility */
export async function isolate(config?: IsolateConfig): Promise<string> {
return createIsolator(config)();
}
2 changes: 0 additions & 2 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { inspectValue, readTypedJsonSync } from "./utils";
export type IsolateConfigResolved = {
buildDirName?: string;
includeDevDependencies: boolean;
includePatchedDependencies: boolean;
isolateDirName: string;
logLevel: LogLevel;
targetPackagePath?: string;
Expand All @@ -25,7 +24,6 @@ export type IsolateConfig = Partial<IsolateConfigResolved>;
const configDefaults: IsolateConfigResolved = {
buildDirName: undefined,
includeDevDependencies: false,
includePatchedDependencies: false,
isolateDirName: "isolate",
logLevel: "info",
targetPackagePath: undefined,
Expand Down
22 changes: 8 additions & 14 deletions src/lib/lockfile/helpers/generate-pnpm-lockfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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 } from "~/lib/types";
import type { PackageManifest, PackagesRegistry, PatchFile } from "~/lib/types";
import { getErrorMessage, isRushWorkspace } from "~/lib/utils";
import { pnpmMapImporter } from "./pnpm-map-importer";

Expand All @@ -27,7 +27,7 @@ export async function generatePnpmLockfile({
targetPackageManifest,
majorVersion,
includeDevDependencies,
includePatchedDependencies,
patchedDependencies,
}: {
workspaceRootDir: string;
targetPackageDir: string;
Expand All @@ -37,7 +37,8 @@ export async function generatePnpmLockfile({
targetPackageManifest: PackageManifest;
majorVersion: number;
includeDevDependencies: boolean;
includePatchedDependencies: 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
Expand Down Expand Up @@ -132,7 +133,6 @@ export async function generatePnpmLockfile({
".",
pnpmMapImporter(".", importer!, {
includeDevDependencies,
includePatchedDependencies,
directoryByPackageName,
}),
];
Expand All @@ -143,8 +143,7 @@ export async function generatePnpmLockfile({
return [
importerId,
pnpmMapImporter(importerId, importer!, {
includeDevDependencies: false, // Only include dev deps for target package
includePatchedDependencies,
includeDevDependencies: false,
directoryByPackageName,
}),
];
Expand All @@ -163,15 +162,10 @@ 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.
* Use pre-computed patched dependencies with transformed paths. The paths
* are already adapted by copyPatches to match the isolated directory
* structure (flattened to patches/ with collision avoidance).
*/
const patchedDependencies = includePatchedDependencies
? lockfile.patchedDependencies
: undefined;

if (useVersion9) {
await writeWantedLockfile_v9(isolateDir, {
...prunedLockfile,
Expand Down
3 changes: 1 addition & 2 deletions src/lib/lockfile/helpers/pnpm-map-importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export function pnpmMapImporter(
directoryByPackageName,
}: {
includeDevDependencies: boolean;
includePatchedDependencies: boolean;
directoryByPackageName: { [packageName: string]: string };
}
): ProjectSnapshot {
Expand Down Expand Up @@ -50,7 +49,7 @@ function pnpmMapDependenciesLinks(
return value;
}

// Replace backslashes with forward slashes to support Windows Git Bash
/** Replace backslashes with forward slashes to support Windows Git Bash */
const relativePath = path
.relative(importerPath, got(directoryByPackageName, key))
.replace(path.sep, path.posix.sep);
Expand Down
7 changes: 5 additions & 2 deletions src/lib/lockfile/process-lockfile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IsolateConfigResolved } from "../config";
import { useLogger } from "../logger";
import { usePackageManager } from "../package-manager";
import type { PackageManifest, PackagesRegistry } from "../types";
import type { PackageManifest, PackagesRegistry, PatchFile } from "../types";
import {
generateNpmLockfile,
generatePnpmLockfile,
Expand All @@ -22,6 +22,7 @@ export async function processLockfile({
internalDepPackageNames,
targetPackageDir,
targetPackageManifest,
patchedDependencies,
config,
}: {
workspaceRootDir: string;
Expand All @@ -31,6 +32,8 @@ export async function processLockfile({
targetPackageDir: string;
targetPackageName: string;
targetPackageManifest: PackageManifest;
/** Pre-computed patched dependencies with transformed paths from copyPatches */
patchedDependencies?: Record<string, PatchFile>;
config: IsolateConfigResolved;
}) {
const log = useLogger();
Expand Down Expand Up @@ -89,7 +92,7 @@ export async function processLockfile({
targetPackageManifest,
majorVersion,
includeDevDependencies: config.includeDevDependencies,
includePatchedDependencies: config.includePatchedDependencies,
patchedDependencies,
});
break;
}
Expand Down
Loading