isolate-package 1.31.0 → 1.32.1

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.
Files changed (30) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/{isolate-DtNAHzfa.mjs → isolate-DyRD5Zd_.mjs} +442 -124
  3. package/dist/isolate-DyRD5Zd_.mjs.map +1 -0
  4. package/dist/isolate-bin.mjs +1 -1
  5. package/package.json +1 -1
  6. package/src/isolate.ts +7 -4
  7. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/package-lock.json +82 -0
  8. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/package.json +8 -0
  9. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/api/package.json +12 -0
  10. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/shared/package.json +12 -0
  11. package/src/lib/lockfile/helpers/__fixtures__/internal-deps/workspace/packages/utils/package.json +11 -0
  12. package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/package-lock.json +56 -0
  13. package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/package.json +8 -0
  14. package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/packages/api/package.json +11 -0
  15. package/src/lib/lockfile/helpers/__fixtures__/nested-version-override/workspace/packages/other/package.json +11 -0
  16. package/src/lib/lockfile/helpers/generate-npm-lockfile.integration.test.ts +243 -0
  17. package/src/lib/lockfile/helpers/generate-npm-lockfile.test.ts +604 -0
  18. package/src/lib/lockfile/helpers/generate-npm-lockfile.ts +417 -21
  19. package/src/lib/lockfile/process-lockfile.test.ts +4 -0
  20. package/src/lib/lockfile/process-lockfile.ts +14 -16
  21. package/src/lib/patches/copy-patches.test.ts +78 -0
  22. package/src/lib/patches/copy-patches.ts +22 -1
  23. package/src/lib/patches/write-isolate-pnpm-workspace.test.ts +189 -0
  24. package/src/lib/patches/write-isolate-pnpm-workspace.ts +80 -0
  25. package/src/lib/registry/collect-reachable-package-names.test.ts +239 -0
  26. package/src/lib/registry/collect-reachable-package-names.ts +60 -0
  27. package/src/lib/registry/index.ts +1 -0
  28. package/src/lib/utils/filter-patched-dependencies.test.ts +77 -0
  29. package/src/lib/utils/filter-patched-dependencies.ts +41 -17
  30. package/dist/isolate-DtNAHzfa.mjs.map +0 -1
@@ -67,6 +67,7 @@ describe("copyPatches", () => {
67
67
  workspaceRootDir: "/workspace",
68
68
  targetPackageManifest: { name: "test", version: "1.0.0" },
69
69
  isolateDir: "/workspace/isolate",
70
+ packagesRegistry: {},
70
71
  includeDevDependencies: false,
71
72
  });
72
73
 
@@ -127,6 +128,7 @@ describe("copyPatches", () => {
127
128
  workspaceRootDir: "/workspace",
128
129
  targetPackageManifest: { name: "test", version: "1.0.0" },
129
130
  isolateDir: "/workspace/isolate",
131
+ packagesRegistry: {},
130
132
  includeDevDependencies: false,
131
133
  });
132
134
 
@@ -146,6 +148,7 @@ describe("copyPatches", () => {
146
148
  workspaceRootDir: "/workspace",
147
149
  targetPackageManifest: { name: "test", version: "1.0.0" },
148
150
  isolateDir: "/workspace/isolate",
151
+ packagesRegistry: {},
149
152
  includeDevDependencies: false,
150
153
  });
151
154
 
@@ -175,6 +178,7 @@ describe("copyPatches", () => {
175
178
  workspaceRootDir: "/workspace",
176
179
  targetPackageManifest: targetManifest,
177
180
  isolateDir: "/workspace/isolate",
181
+ packagesRegistry: {},
178
182
  includeDevDependencies: false,
179
183
  });
180
184
 
@@ -212,6 +216,7 @@ describe("copyPatches", () => {
212
216
  workspaceRootDir: "/workspace",
213
217
  targetPackageManifest: targetManifest,
214
218
  isolateDir: "/workspace/isolate",
219
+ packagesRegistry: {},
215
220
  includeDevDependencies: true,
216
221
  });
217
222
 
@@ -222,6 +227,7 @@ describe("copyPatches", () => {
222
227
  patchedDependencies: { "vitest@1.0.0": "patches/vitest.patch" },
223
228
  targetPackageManifest: targetManifest,
224
229
  includeDevDependencies: true,
230
+ reachableDependencyNames: expect.any(Set),
225
231
  });
226
232
  expect(fs.copy).toHaveBeenCalledWith(
227
233
  "/workspace/patches/vitest.patch",
@@ -252,6 +258,7 @@ describe("copyPatches", () => {
252
258
  workspaceRootDir: "/workspace",
253
259
  targetPackageManifest: targetManifest,
254
260
  isolateDir: "/workspace/isolate",
261
+ packagesRegistry: {},
255
262
  includeDevDependencies: false,
256
263
  });
257
264
 
@@ -282,6 +289,7 @@ describe("copyPatches", () => {
282
289
  workspaceRootDir: "/workspace",
283
290
  targetPackageManifest: targetManifest,
284
291
  isolateDir: "/workspace/isolate",
292
+ packagesRegistry: {},
285
293
  includeDevDependencies: false,
286
294
  });
287
295
 
@@ -315,6 +323,7 @@ describe("copyPatches", () => {
315
323
  workspaceRootDir: "/workspace",
316
324
  targetPackageManifest: targetManifest,
317
325
  isolateDir: "/workspace/isolate",
326
+ packagesRegistry: {},
318
327
  includeDevDependencies: false,
319
328
  });
320
329
 
@@ -357,6 +366,7 @@ describe("copyPatches", () => {
357
366
  workspaceRootDir: "/workspace",
358
367
  targetPackageManifest: targetManifest,
359
368
  isolateDir: "/workspace/isolate",
369
+ packagesRegistry: {},
360
370
  includeDevDependencies: false,
361
371
  });
362
372
 
@@ -401,6 +411,7 @@ describe("copyPatches", () => {
401
411
  workspaceRootDir: "/workspace",
402
412
  targetPackageManifest: targetManifest,
403
413
  isolateDir: "/workspace/isolate",
414
+ packagesRegistry: {},
404
415
  includeDevDependencies: false,
405
416
  });
406
417
 
@@ -447,6 +458,7 @@ describe("copyPatches", () => {
447
458
  workspaceRootDir: "/workspace",
448
459
  targetPackageManifest: targetManifest,
449
460
  isolateDir: "/workspace/isolate",
461
+ packagesRegistry: {},
450
462
  includeDevDependencies: false,
451
463
  });
452
464
 
@@ -458,4 +470,70 @@ describe("copyPatches", () => {
458
470
  "/workspace/isolate/patches/lodash.patch",
459
471
  );
460
472
  });
473
+
474
+ it("should pass reachable transitive dep names from internal packages to the filter (regression: issue #167)", async () => {
475
+ /**
476
+ * Target `consumer` depends on internal `firebase-package`, which in turn
477
+ * depends on `tslib`. A patch for `tslib@2.0.0` declared at the workspace
478
+ * root must reach the filter with `tslib` in `reachableDependencyNames`
479
+ * so it can be preserved even though `consumer` doesn't list it directly.
480
+ */
481
+ const consumerManifest: PackageManifest = {
482
+ name: "consumer",
483
+ version: "1.0.0",
484
+ dependencies: { "firebase-package": "file:./packages/firebase-package" },
485
+ };
486
+
487
+ readTypedYamlSync.mockReturnValue({
488
+ patchedDependencies: {
489
+ "tslib@2.0.0": "patches/tslib@2.0.0.patch",
490
+ },
491
+ });
492
+ readTypedJson.mockResolvedValue({
493
+ name: "root",
494
+ version: "1.0.0",
495
+ } as PackageManifest);
496
+
497
+ filterPatchedDependencies.mockReturnValue({
498
+ "tslib@2.0.0": "patches/tslib@2.0.0.patch",
499
+ });
500
+
501
+ fs.existsSync.mockReturnValue(true);
502
+
503
+ usePackageManager.mockReturnValue({
504
+ name: "pnpm",
505
+ majorVersion: 9,
506
+ version: "9.0.0",
507
+ packageManagerString: "pnpm@9.0.0",
508
+ });
509
+
510
+ const result = await copyPatches({
511
+ workspaceRootDir: "/workspace",
512
+ targetPackageManifest: consumerManifest,
513
+ isolateDir: "/workspace/isolate",
514
+ packagesRegistry: {
515
+ "firebase-package": {
516
+ absoluteDir: "/workspace/packages/firebase-package",
517
+ rootRelativeDir: "packages/firebase-package",
518
+ manifest: {
519
+ name: "firebase-package",
520
+ version: "1.0.0",
521
+ dependencies: { tslib: "^2.0.0" },
522
+ },
523
+ },
524
+ },
525
+ includeDevDependencies: false,
526
+ });
527
+
528
+ expect(result).toEqual({
529
+ "tslib@2.0.0": { path: "patches/tslib@2.0.0.patch", hash: "" },
530
+ });
531
+
532
+ const filterCall = filterPatchedDependencies.mock.calls[0]?.[0];
533
+ expect(filterCall).toBeDefined();
534
+ const reachable = filterCall!.reachableDependencyNames;
535
+ expect(reachable).toBeInstanceOf(Set);
536
+ expect(reachable!.has("firebase-package")).toBe(true);
537
+ expect(reachable!.has("tslib")).toBe(true);
538
+ });
461
539
  });
@@ -4,7 +4,13 @@ import { readWantedLockfile as readWantedLockfile_v8 } from "pnpm_lockfile_file_
4
4
  import { readWantedLockfile as readWantedLockfile_v9 } from "pnpm_lockfile_file_v9";
5
5
  import { useLogger } from "~/lib/logger";
6
6
  import { usePackageManager } from "~/lib/package-manager";
7
- import type { PackageManifest, PatchFile, PnpmSettings } from "~/lib/types";
7
+ import { collectReachablePackageNames } from "~/lib/registry";
8
+ import type {
9
+ PackageManifest,
10
+ PackagesRegistry,
11
+ PatchFile,
12
+ PnpmSettings,
13
+ } from "~/lib/types";
8
14
  import {
9
15
  filterPatchedDependencies,
10
16
  getRootRelativeLogPath,
@@ -16,11 +22,13 @@ import {
16
22
  export async function copyPatches({
17
23
  workspaceRootDir,
18
24
  targetPackageManifest,
25
+ packagesRegistry,
19
26
  isolateDir,
20
27
  includeDevDependencies,
21
28
  }: {
22
29
  workspaceRootDir: string;
23
30
  targetPackageManifest: PackageManifest;
31
+ packagesRegistry: PackagesRegistry;
24
32
  isolateDir: string;
25
33
  includeDevDependencies: boolean;
26
34
  }): Promise<Record<string, PatchFile>> {
@@ -82,10 +90,23 @@ export async function copyPatches({
82
90
  `Found ${Object.keys(patchedDependencies).length} patched dependencies in workspace`,
83
91
  );
84
92
 
93
+ /**
94
+ * Collect the set of dependency names reachable from the target (direct deps
95
+ * plus deps introduced by internal workspace packages). Patches for names in
96
+ * this set are preserved even when the target doesn't list them directly —
97
+ * see issue #167.
98
+ */
99
+ const reachableDependencyNames = collectReachablePackageNames({
100
+ targetPackageManifest,
101
+ packagesRegistry,
102
+ includeDevDependencies,
103
+ });
104
+
85
105
  const filteredPatches = filterPatchedDependencies({
86
106
  patchedDependencies,
87
107
  targetPackageManifest,
88
108
  includeDevDependencies,
109
+ reachableDependencyNames,
89
110
  });
90
111
 
91
112
  if (!filteredPatches) {
@@ -0,0 +1,189 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { PatchFile } from "~/lib/types";
3
+ import { writeIsolatePnpmWorkspace } from "./write-isolate-pnpm-workspace";
4
+
5
+ vi.mock("fs-extra", () => ({
6
+ default: {
7
+ copyFileSync: vi.fn(),
8
+ },
9
+ }));
10
+
11
+ vi.mock("~/lib/utils", () => ({
12
+ readTypedYamlSync: vi.fn(),
13
+ writeTypedYamlSync: vi.fn(),
14
+ }));
15
+
16
+ const fs = vi.mocked((await import("fs-extra")).default);
17
+ const { readTypedYamlSync, writeTypedYamlSync } = vi.mocked(
18
+ await import("~/lib/utils"),
19
+ );
20
+
21
+ const workspaceRootDir = "/workspace";
22
+ const isolateDir = "/workspace/isolate";
23
+
24
+ describe("writeIsolatePnpmWorkspace", () => {
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ afterEach(() => {
30
+ vi.restoreAllMocks();
31
+ });
32
+
33
+ it("retains only the patches that were copied", () => {
34
+ readTypedYamlSync.mockReturnValue({
35
+ packages: ["packages/*"],
36
+ patchedDependencies: {
37
+ "lodash@4.17.21": "patches/lodash@4.17.21.patch",
38
+ "react@18.2.0": "patches/react@18.2.0.patch",
39
+ "axios@1.6.0": "patches/axios@1.6.0.patch",
40
+ },
41
+ });
42
+
43
+ const copiedPatches: Record<string, PatchFile> = {
44
+ "lodash@4.17.21": {
45
+ path: "patches/lodash@4.17.21.patch",
46
+ hash: "abc",
47
+ },
48
+ };
49
+
50
+ writeIsolatePnpmWorkspace({
51
+ workspaceRootDir,
52
+ isolateDir,
53
+ copiedPatches,
54
+ });
55
+
56
+ expect(fs.copyFileSync).not.toHaveBeenCalled();
57
+ expect(writeTypedYamlSync).toHaveBeenCalledTimes(1);
58
+ expect(writeTypedYamlSync).toHaveBeenCalledWith(
59
+ "/workspace/isolate/pnpm-workspace.yaml",
60
+ {
61
+ packages: ["packages/*"],
62
+ patchedDependencies: {
63
+ "lodash@4.17.21": "patches/lodash@4.17.21.patch",
64
+ },
65
+ },
66
+ );
67
+ });
68
+
69
+ it("removes the patchedDependencies field when no patches were copied", () => {
70
+ readTypedYamlSync.mockReturnValue({
71
+ packages: ["packages/*"],
72
+ patchedDependencies: {
73
+ "lodash@4.17.21": "patches/lodash@4.17.21.patch",
74
+ },
75
+ });
76
+
77
+ writeIsolatePnpmWorkspace({
78
+ workspaceRootDir,
79
+ isolateDir,
80
+ copiedPatches: {},
81
+ });
82
+
83
+ expect(fs.copyFileSync).not.toHaveBeenCalled();
84
+ expect(writeTypedYamlSync).toHaveBeenCalledWith(
85
+ "/workspace/isolate/pnpm-workspace.yaml",
86
+ { packages: ["packages/*"] },
87
+ );
88
+ });
89
+
90
+ it("falls back to a verbatim copy when the file has no patchedDependencies field", () => {
91
+ readTypedYamlSync.mockReturnValue({
92
+ packages: ["packages/*"],
93
+ });
94
+
95
+ writeIsolatePnpmWorkspace({
96
+ workspaceRootDir,
97
+ isolateDir,
98
+ copiedPatches: {},
99
+ });
100
+
101
+ expect(writeTypedYamlSync).not.toHaveBeenCalled();
102
+ expect(fs.copyFileSync).toHaveBeenCalledWith(
103
+ "/workspace/pnpm-workspace.yaml",
104
+ "/workspace/isolate/pnpm-workspace.yaml",
105
+ );
106
+ });
107
+
108
+ it("preserves unrelated top-level fields", () => {
109
+ readTypedYamlSync.mockReturnValue({
110
+ packages: ["packages/*"],
111
+ onlyBuiltDependencies: ["esbuild"],
112
+ overrides: { foo: "1.0.0" },
113
+ patchedDependencies: {
114
+ "lodash@4.17.21": "patches/lodash@4.17.21.patch",
115
+ "react@18.2.0": "patches/react@18.2.0.patch",
116
+ },
117
+ });
118
+
119
+ const copiedPatches: Record<string, PatchFile> = {
120
+ "react@18.2.0": { path: "patches/react@18.2.0.patch", hash: "def" },
121
+ };
122
+
123
+ writeIsolatePnpmWorkspace({
124
+ workspaceRootDir,
125
+ isolateDir,
126
+ copiedPatches,
127
+ });
128
+
129
+ expect(writeTypedYamlSync).toHaveBeenCalledWith(
130
+ "/workspace/isolate/pnpm-workspace.yaml",
131
+ {
132
+ packages: ["packages/*"],
133
+ onlyBuiltDependencies: ["esbuild"],
134
+ overrides: { foo: "1.0.0" },
135
+ patchedDependencies: {
136
+ "react@18.2.0": "patches/react@18.2.0.patch",
137
+ },
138
+ },
139
+ );
140
+ });
141
+
142
+ it("copies verbatim when every patch is kept (preserving comments and order)", () => {
143
+ readTypedYamlSync.mockReturnValue({
144
+ packages: ["packages/*"],
145
+ patchedDependencies: {
146
+ "lodash@4.17.21": "patches/lodash@4.17.21.patch",
147
+ "react@18.2.0": "patches/react@18.2.0.patch",
148
+ },
149
+ });
150
+
151
+ const copiedPatches: Record<string, PatchFile> = {
152
+ "lodash@4.17.21": {
153
+ path: "patches/lodash@4.17.21.patch",
154
+ hash: "abc",
155
+ },
156
+ "react@18.2.0": { path: "patches/react@18.2.0.patch", hash: "def" },
157
+ };
158
+
159
+ writeIsolatePnpmWorkspace({
160
+ workspaceRootDir,
161
+ isolateDir,
162
+ copiedPatches,
163
+ });
164
+
165
+ expect(writeTypedYamlSync).not.toHaveBeenCalled();
166
+ expect(fs.copyFileSync).toHaveBeenCalledWith(
167
+ "/workspace/pnpm-workspace.yaml",
168
+ "/workspace/isolate/pnpm-workspace.yaml",
169
+ );
170
+ });
171
+
172
+ it("falls back to a verbatim copy when the yaml cannot be parsed", () => {
173
+ readTypedYamlSync.mockImplementation(() => {
174
+ throw new Error("bad yaml");
175
+ });
176
+
177
+ writeIsolatePnpmWorkspace({
178
+ workspaceRootDir,
179
+ isolateDir,
180
+ copiedPatches: {},
181
+ });
182
+
183
+ expect(writeTypedYamlSync).not.toHaveBeenCalled();
184
+ expect(fs.copyFileSync).toHaveBeenCalledWith(
185
+ "/workspace/pnpm-workspace.yaml",
186
+ "/workspace/isolate/pnpm-workspace.yaml",
187
+ );
188
+ });
189
+ });
@@ -0,0 +1,80 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { useLogger } from "~/lib/logger";
4
+ import type { PatchFile, PnpmSettings } from "~/lib/types";
5
+ import { readTypedYamlSync, writeTypedYamlSync } from "~/lib/utils";
6
+
7
+ /**
8
+ * Copy `pnpm-workspace.yaml` from the workspace root to the isolate directory,
9
+ * filtering its `patchedDependencies` field so it only references patches that
10
+ * were actually copied to the isolate. Without this, `pnpm install` in the
11
+ * isolate fails when patches that don't apply to the target package are
12
+ * declared in the workspace root config (see issue #178).
13
+ *
14
+ * The yaml is only rewritten when filtering is required. The file is copied
15
+ * verbatim — preserving comments, key order, and trailing whitespace — when
16
+ * any of the following hold:
17
+ *
18
+ * - The source yaml cannot be read or parsed.
19
+ * - The parsed settings have no `patchedDependencies` field.
20
+ * - Every entry in `patchedDependencies` is also present in `copiedPatches`
21
+ * (no exclusions, so rewriting would only churn formatting).
22
+ *
23
+ * Otherwise, `patchedDependencies` is rewritten to the entries in
24
+ * `copiedPatches` (or removed entirely when none remain).
25
+ */
26
+ export function writeIsolatePnpmWorkspace({
27
+ workspaceRootDir,
28
+ isolateDir,
29
+ copiedPatches,
30
+ }: {
31
+ workspaceRootDir: string;
32
+ isolateDir: string;
33
+ copiedPatches: Record<string, PatchFile>;
34
+ }) {
35
+ const log = useLogger();
36
+ const sourcePath = path.join(workspaceRootDir, "pnpm-workspace.yaml");
37
+ const targetPath = path.join(isolateDir, "pnpm-workspace.yaml");
38
+
39
+ let settings: PnpmSettings | undefined;
40
+
41
+ try {
42
+ settings = readTypedYamlSync<PnpmSettings>(sourcePath);
43
+ } catch (error) {
44
+ log.warn(
45
+ `Could not read pnpm-workspace.yaml, falling back to verbatim copy: ${error instanceof Error ? error.message : String(error)}`,
46
+ );
47
+ fs.copyFileSync(sourcePath, targetPath);
48
+ return;
49
+ }
50
+
51
+ if (!settings || !settings.patchedDependencies) {
52
+ fs.copyFileSync(sourcePath, targetPath);
53
+ return;
54
+ }
55
+
56
+ /**
57
+ * If every patch declared in the source yaml was kept, copy verbatim so
58
+ * comments, ordering, and trailing whitespace are preserved.
59
+ */
60
+ const sourceSpecs = Object.keys(settings.patchedDependencies);
61
+ const copiedSpecs = new Set(Object.keys(copiedPatches));
62
+ const hasExclusions = sourceSpecs.some((spec) => !copiedSpecs.has(spec));
63
+
64
+ if (!hasExclusions) {
65
+ fs.copyFileSync(sourcePath, targetPath);
66
+ return;
67
+ }
68
+
69
+ const filteredEntries = Object.entries(copiedPatches).map(
70
+ ([spec, patchFile]) => [spec, patchFile.path] as const,
71
+ );
72
+
73
+ if (filteredEntries.length > 0) {
74
+ settings.patchedDependencies = Object.fromEntries(filteredEntries);
75
+ } else {
76
+ delete settings.patchedDependencies;
77
+ }
78
+
79
+ writeTypedYamlSync(targetPath, settings);
80
+ }