isolate-package 1.32.0 → 1.33.0-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-BRD2AgVJ.mjs → isolate-DTwgcMAN.mjs} +357 -36
- package/dist/isolate-DTwgcMAN.mjs.map +1 -0
- package/dist/isolate-bin.mjs +1 -1
- package/package.json +1 -1
- package/src/isolate.ts +8 -4
- package/src/lib/lockfile/helpers/bun-lockfile.ts +143 -0
- package/src/lib/lockfile/helpers/generate-bun-lockfile.ts +7 -139
- package/src/lib/patches/collect-installed-names-bun.test.ts +154 -0
- package/src/lib/patches/collect-installed-names-bun.ts +87 -0
- package/src/lib/patches/collect-installed-names-pnpm.test.ts +316 -0
- package/src/lib/patches/collect-installed-names-pnpm.ts +364 -0
- package/src/lib/patches/copy-patches.test.ts +125 -0
- package/src/lib/patches/copy-patches.ts +38 -1
- package/src/lib/patches/write-isolate-pnpm-workspace.test.ts +189 -0
- package/src/lib/patches/write-isolate-pnpm-workspace.ts +80 -0
- package/dist/isolate-BRD2AgVJ.mjs.map +0 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { collectInstalledNamesFromPnpmLockfile } from "./collect-installed-names-pnpm";
|
|
3
|
+
|
|
4
|
+
vi.mock("pnpm_lockfile_file_v8", () => ({
|
|
5
|
+
readWantedLockfile: vi.fn(() => Promise.resolve(null)),
|
|
6
|
+
getLockfileImporterId: vi.fn(
|
|
7
|
+
(root: string, dir: string) => dir.replace(`${root}/`, "") || ".",
|
|
8
|
+
),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("pnpm_lockfile_file_v9", () => ({
|
|
12
|
+
readWantedLockfile: vi.fn(() => Promise.resolve(null)),
|
|
13
|
+
getLockfileImporterId: vi.fn(
|
|
14
|
+
(root: string, dir: string) => dir.replace(`${root}/`, "") || ".",
|
|
15
|
+
),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("~/lib/utils", () => ({
|
|
19
|
+
getPackageName: vi.fn((spec: string) => {
|
|
20
|
+
if (spec.startsWith("@")) {
|
|
21
|
+
const parts = spec.split("@");
|
|
22
|
+
return `@${parts[1] ?? ""}`;
|
|
23
|
+
}
|
|
24
|
+
return spec.split("@")[0] ?? "";
|
|
25
|
+
}),
|
|
26
|
+
isRushWorkspace: vi.fn(() => false),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const { readWantedLockfile: readWantedLockfile_v9 } = vi.mocked(
|
|
30
|
+
await import("pnpm_lockfile_file_v9"),
|
|
31
|
+
);
|
|
32
|
+
const { readWantedLockfile: readWantedLockfile_v8 } = vi.mocked(
|
|
33
|
+
await import("pnpm_lockfile_file_v8"),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const baseArgs = {
|
|
37
|
+
workspaceRootDir: "/workspace",
|
|
38
|
+
targetPackageDir: "/workspace/packages/consumer",
|
|
39
|
+
internalDepPackageNames: [],
|
|
40
|
+
packagesRegistry: {},
|
|
41
|
+
includeDevDependencies: false,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe("collectInstalledNamesFromPnpmLockfile", () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns an empty set when the lockfile is missing", async () => {
|
|
54
|
+
readWantedLockfile_v9.mockResolvedValue(null);
|
|
55
|
+
|
|
56
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
57
|
+
...baseArgs,
|
|
58
|
+
majorVersion: 9,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual(new Set());
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("walks external-to-external transitives from the target importer", async () => {
|
|
65
|
+
readWantedLockfile_v9.mockResolvedValue({
|
|
66
|
+
lockfileVersion: "9.0",
|
|
67
|
+
importers: {
|
|
68
|
+
"packages/consumer": {
|
|
69
|
+
specifiers: { "@react-pdf/renderer": "^4.0.0" },
|
|
70
|
+
dependencies: { "@react-pdf/renderer": "4.0.0" },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
packages: {
|
|
74
|
+
"@react-pdf/renderer@4.0.0": {
|
|
75
|
+
resolution: { integrity: "sha512-x" },
|
|
76
|
+
dependencies: { "@react-pdf/render": "4.3.0" },
|
|
77
|
+
},
|
|
78
|
+
"@react-pdf/render@4.3.0": {
|
|
79
|
+
resolution: { integrity: "sha512-y" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
} as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
|
|
83
|
+
|
|
84
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
85
|
+
...baseArgs,
|
|
86
|
+
majorVersion: 9,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.has("@react-pdf/renderer")).toBe(true);
|
|
90
|
+
expect(result.has("@react-pdf/render")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("walks transitives reachable through internal workspace importers", async () => {
|
|
94
|
+
readWantedLockfile_v9.mockResolvedValue({
|
|
95
|
+
lockfileVersion: "9.0",
|
|
96
|
+
importers: {
|
|
97
|
+
"packages/consumer": {
|
|
98
|
+
specifiers: { "firebase-package": "workspace:*" },
|
|
99
|
+
dependencies: { "firebase-package": "link:../firebase-package" },
|
|
100
|
+
},
|
|
101
|
+
"packages/firebase-package": {
|
|
102
|
+
specifiers: { tslib: "^2.0.0" },
|
|
103
|
+
dependencies: { tslib: "2.0.0" },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
packages: {
|
|
107
|
+
"tslib@2.0.0": { resolution: { integrity: "sha512-z" } },
|
|
108
|
+
},
|
|
109
|
+
} as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
|
|
110
|
+
|
|
111
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
112
|
+
...baseArgs,
|
|
113
|
+
internalDepPackageNames: ["firebase-package"],
|
|
114
|
+
packagesRegistry: {
|
|
115
|
+
"firebase-package": {
|
|
116
|
+
absoluteDir: "/workspace/packages/firebase-package",
|
|
117
|
+
rootRelativeDir: "packages/firebase-package",
|
|
118
|
+
manifest: { name: "firebase-package", version: "1.0.0" },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
majorVersion: 9,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.has("tslib")).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("does not include the target's devDependencies when includeDevDependencies is false", async () => {
|
|
128
|
+
readWantedLockfile_v9.mockResolvedValue({
|
|
129
|
+
lockfileVersion: "9.0",
|
|
130
|
+
importers: {
|
|
131
|
+
"packages/consumer": {
|
|
132
|
+
specifiers: { lodash: "^4.0.0", typescript: "^5.0.0" },
|
|
133
|
+
dependencies: { lodash: "4.17.21" },
|
|
134
|
+
devDependencies: { typescript: "5.5.0" },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
packages: {
|
|
138
|
+
"lodash@4.17.21": { resolution: { integrity: "sha512-a" } },
|
|
139
|
+
"typescript@5.5.0": { resolution: { integrity: "sha512-b" } },
|
|
140
|
+
},
|
|
141
|
+
} as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
|
|
142
|
+
|
|
143
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
144
|
+
...baseArgs,
|
|
145
|
+
majorVersion: 9,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result.has("lodash")).toBe(true);
|
|
149
|
+
expect(result.has("typescript")).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("includes the target's devDependencies when includeDevDependencies is true", async () => {
|
|
153
|
+
readWantedLockfile_v9.mockResolvedValue({
|
|
154
|
+
lockfileVersion: "9.0",
|
|
155
|
+
importers: {
|
|
156
|
+
"packages/consumer": {
|
|
157
|
+
specifiers: { typescript: "^5.0.0" },
|
|
158
|
+
devDependencies: { typescript: "5.5.0" },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
packages: {
|
|
162
|
+
"typescript@5.5.0": { resolution: { integrity: "sha512-b" } },
|
|
163
|
+
},
|
|
164
|
+
} as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
|
|
165
|
+
|
|
166
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
167
|
+
...baseArgs,
|
|
168
|
+
majorVersion: 9,
|
|
169
|
+
includeDevDependencies: true,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result.has("typescript")).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("walks transitives via v8 v5-style depPath keys for pnpm major < 9", async () => {
|
|
176
|
+
/**
|
|
177
|
+
* After `readWantedLockfile_v8` normalizes a pnpm 8 lockfile (lockfile
|
|
178
|
+
* version 6.x), `lockfile.packages` is keyed in v5 form: leading slash
|
|
179
|
+
* with `/` separator between name and version, e.g. `/foo/1.0.0` and
|
|
180
|
+
* `/@scope/foo/1.0.0`.
|
|
181
|
+
*/
|
|
182
|
+
readWantedLockfile_v8.mockResolvedValue({
|
|
183
|
+
lockfileVersion: 6.1,
|
|
184
|
+
importers: {
|
|
185
|
+
"packages/consumer": {
|
|
186
|
+
specifiers: { "@react-pdf/renderer": "^4.0.0" },
|
|
187
|
+
dependencies: { "@react-pdf/renderer": "4.0.0" },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
packages: {
|
|
191
|
+
"/@react-pdf/renderer/4.0.0": {
|
|
192
|
+
resolution: { integrity: "sha512-x" },
|
|
193
|
+
dependencies: { "@react-pdf/render": "4.3.0" },
|
|
194
|
+
},
|
|
195
|
+
"/@react-pdf/render/4.3.0": {
|
|
196
|
+
resolution: { integrity: "sha512-y" },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
} as unknown as Awaited<ReturnType<typeof readWantedLockfile_v8>>);
|
|
200
|
+
|
|
201
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
202
|
+
...baseArgs,
|
|
203
|
+
majorVersion: 8,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(readWantedLockfile_v8).toHaveBeenCalled();
|
|
207
|
+
expect(readWantedLockfile_v9).not.toHaveBeenCalled();
|
|
208
|
+
expect(result.has("@react-pdf/renderer")).toBe(true);
|
|
209
|
+
expect(result.has("@react-pdf/render")).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("includes peerDependencies of package snapshots in the name set", async () => {
|
|
213
|
+
/**
|
|
214
|
+
* Peer requirement values aren't resolved depPaths, so we just collect
|
|
215
|
+
* the names. This mirrors `collectReachablePackageNames` and the bun
|
|
216
|
+
* walker, both of which include peerDependencies.
|
|
217
|
+
*/
|
|
218
|
+
readWantedLockfile_v9.mockResolvedValue({
|
|
219
|
+
lockfileVersion: "9.0",
|
|
220
|
+
importers: {
|
|
221
|
+
"packages/consumer": {
|
|
222
|
+
specifiers: { "some-pkg": "^1.0.0" },
|
|
223
|
+
dependencies: { "some-pkg": "1.0.0" },
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
packages: {
|
|
227
|
+
"some-pkg@1.0.0": {
|
|
228
|
+
resolution: { integrity: "sha512-p" },
|
|
229
|
+
peerDependencies: { "peer-only-dep": ">=1" },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
} as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
|
|
233
|
+
|
|
234
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
235
|
+
...baseArgs,
|
|
236
|
+
majorVersion: 9,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(result.has("some-pkg")).toBe(true);
|
|
240
|
+
expect(result.has("peer-only-dep")).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("strips peer-resolution suffixes when extracting package names", async () => {
|
|
244
|
+
readWantedLockfile_v9.mockResolvedValue({
|
|
245
|
+
lockfileVersion: "9.0",
|
|
246
|
+
importers: {
|
|
247
|
+
"packages/consumer": {
|
|
248
|
+
specifiers: { "react-dom": "^18.0.0" },
|
|
249
|
+
dependencies: {
|
|
250
|
+
"react-dom": "18.2.0(react@18.2.0)",
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
packages: {
|
|
255
|
+
"react-dom@18.2.0(react@18.2.0)": {
|
|
256
|
+
resolution: { integrity: "sha512-d" },
|
|
257
|
+
dependencies: { react: "18.2.0" },
|
|
258
|
+
},
|
|
259
|
+
"react@18.2.0": { resolution: { integrity: "sha512-e" } },
|
|
260
|
+
},
|
|
261
|
+
} as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
|
|
262
|
+
|
|
263
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
264
|
+
...baseArgs,
|
|
265
|
+
majorVersion: 9,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(result.has("react-dom")).toBe(true);
|
|
269
|
+
expect(result.has("react")).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("returns an empty set when the lockfile read throws", async () => {
|
|
273
|
+
readWantedLockfile_v9.mockRejectedValueOnce(new Error("boom"));
|
|
274
|
+
|
|
275
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
276
|
+
...baseArgs,
|
|
277
|
+
majorVersion: 9,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(result).toEqual(new Set());
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("normalizes the target importer id before the isTarget check (Windows)", async () => {
|
|
284
|
+
/**
|
|
285
|
+
* Simulate Windows: getLockfileImporterId returns a backslash-separated
|
|
286
|
+
* id, but the lockfile's importer keys use POSIX separators. Without
|
|
287
|
+
* normalizing the id used in the isTarget comparison, the target's
|
|
288
|
+
* devDependencies would be skipped even with includeDevDependencies=true.
|
|
289
|
+
*/
|
|
290
|
+
const { getLockfileImporterId } = vi.mocked(
|
|
291
|
+
await import("pnpm_lockfile_file_v9"),
|
|
292
|
+
);
|
|
293
|
+
getLockfileImporterId.mockReturnValueOnce("packages\\consumer");
|
|
294
|
+
|
|
295
|
+
readWantedLockfile_v9.mockResolvedValue({
|
|
296
|
+
lockfileVersion: "9.0",
|
|
297
|
+
importers: {
|
|
298
|
+
"packages/consumer": {
|
|
299
|
+
specifiers: { typescript: "^5.0.0" },
|
|
300
|
+
devDependencies: { typescript: "5.5.0" },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
packages: {
|
|
304
|
+
"typescript@5.5.0": { resolution: { integrity: "sha512-w" } },
|
|
305
|
+
},
|
|
306
|
+
} as unknown as Awaited<ReturnType<typeof readWantedLockfile_v9>>);
|
|
307
|
+
|
|
308
|
+
const result = await collectInstalledNamesFromPnpmLockfile({
|
|
309
|
+
...baseArgs,
|
|
310
|
+
majorVersion: 9,
|
|
311
|
+
includeDevDependencies: true,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(result.has("typescript")).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
getLockfileImporterId as getLockfileImporterId_v8,
|
|
4
|
+
readWantedLockfile as readWantedLockfile_v8,
|
|
5
|
+
} from "pnpm_lockfile_file_v8";
|
|
6
|
+
import {
|
|
7
|
+
getLockfileImporterId as getLockfileImporterId_v9,
|
|
8
|
+
readWantedLockfile as readWantedLockfile_v9,
|
|
9
|
+
} from "pnpm_lockfile_file_v9";
|
|
10
|
+
import { useLogger } from "~/lib/logger";
|
|
11
|
+
import type { PackagesRegistry } from "~/lib/types";
|
|
12
|
+
import { getPackageName, isRushWorkspace } from "~/lib/utils";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Walk the workspace pnpm lockfile starting from the target package and its
|
|
16
|
+
* internal workspace dependencies, returning the set of every package name
|
|
17
|
+
* that will end up installed in the isolate (including deep
|
|
18
|
+
* external-to-external transitives).
|
|
19
|
+
*
|
|
20
|
+
* Used by `copyPatches` to preserve patches for transitive deps that aren't
|
|
21
|
+
* directly listed on any internal manifest. Returns an empty set on any
|
|
22
|
+
* failure so the caller falls back to manifest-based reachability. When the
|
|
23
|
+
* lockfile is present but lacks a `packages` section, returns just the
|
|
24
|
+
* direct importer dep names.
|
|
25
|
+
*/
|
|
26
|
+
export async function collectInstalledNamesFromPnpmLockfile({
|
|
27
|
+
workspaceRootDir,
|
|
28
|
+
targetPackageDir,
|
|
29
|
+
internalDepPackageNames,
|
|
30
|
+
packagesRegistry,
|
|
31
|
+
majorVersion,
|
|
32
|
+
includeDevDependencies,
|
|
33
|
+
}: {
|
|
34
|
+
workspaceRootDir: string;
|
|
35
|
+
targetPackageDir: string;
|
|
36
|
+
internalDepPackageNames: string[];
|
|
37
|
+
packagesRegistry: PackagesRegistry;
|
|
38
|
+
majorVersion: number;
|
|
39
|
+
includeDevDependencies: boolean;
|
|
40
|
+
}): Promise<Set<string>> {
|
|
41
|
+
const log = useLogger();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const useVersion9 = majorVersion >= 9;
|
|
45
|
+
const isRush = isRushWorkspace(workspaceRootDir);
|
|
46
|
+
const lockfileDir = isRush
|
|
47
|
+
? path.join(workspaceRootDir, "common/config/rush")
|
|
48
|
+
: workspaceRootDir;
|
|
49
|
+
|
|
50
|
+
const lockfile = useVersion9
|
|
51
|
+
? await readWantedLockfile_v9(lockfileDir, { ignoreIncompatible: false })
|
|
52
|
+
: await readWantedLockfile_v8(lockfileDir, { ignoreIncompatible: false });
|
|
53
|
+
|
|
54
|
+
if (!lockfile) {
|
|
55
|
+
log.debug("No pnpm lockfile available for installed-names walk");
|
|
56
|
+
return new Set();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rawTargetImporterId = useVersion9
|
|
60
|
+
? getLockfileImporterId_v9(workspaceRootDir, targetPackageDir)
|
|
61
|
+
: getLockfileImporterId_v8(workspaceRootDir, targetPackageDir);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Normalize separators to POSIX so Windows callers match the lockfile's
|
|
65
|
+
* importer keys (mirrors generate-pnpm-lockfile.ts). Applied once here so
|
|
66
|
+
* the `isTarget` equality check below compares apples-to-apples — without
|
|
67
|
+
* this, on Windows the raw id with backslashes wouldn't match the
|
|
68
|
+
* normalized id used as the importers map key.
|
|
69
|
+
*/
|
|
70
|
+
const targetImporterId = toLockfileImporterKey(rawTargetImporterId, isRush);
|
|
71
|
+
|
|
72
|
+
const importerIds = [
|
|
73
|
+
targetImporterId,
|
|
74
|
+
...internalDepPackageNames
|
|
75
|
+
.map((name) => packagesRegistry[name]?.rootRelativeDir)
|
|
76
|
+
.filter((dir): dir is string => Boolean(dir))
|
|
77
|
+
.map((dir) => toLockfileImporterKey(dir, isRush)),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const packages = (lockfile as { packages?: Record<string, PnpmPackage> })
|
|
81
|
+
.packages;
|
|
82
|
+
|
|
83
|
+
if (!packages) {
|
|
84
|
+
log.debug("Lockfile has no packages section to walk");
|
|
85
|
+
return collectImporterDirectNames(
|
|
86
|
+
lockfile.importers,
|
|
87
|
+
importerIds,
|
|
88
|
+
targetImporterId,
|
|
89
|
+
includeDevDependencies,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const names = new Set<string>();
|
|
94
|
+
const seen = new Set<string>();
|
|
95
|
+
const queue: string[] = [];
|
|
96
|
+
|
|
97
|
+
for (const importerId of importerIds) {
|
|
98
|
+
const importer = lockfile.importers[importerId];
|
|
99
|
+
if (!importer) continue;
|
|
100
|
+
|
|
101
|
+
const isTarget = importerId === targetImporterId;
|
|
102
|
+
|
|
103
|
+
enqueueImporterDeps({
|
|
104
|
+
importer,
|
|
105
|
+
names,
|
|
106
|
+
queue,
|
|
107
|
+
useVersion9,
|
|
108
|
+
includeDevDependencies: isTarget && includeDevDependencies,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
while (queue.length > 0) {
|
|
113
|
+
const depPath = queue.pop()!;
|
|
114
|
+
if (seen.has(depPath)) continue;
|
|
115
|
+
seen.add(depPath);
|
|
116
|
+
|
|
117
|
+
names.add(extractPackageName(depPath));
|
|
118
|
+
|
|
119
|
+
const pkg = packages[depPath];
|
|
120
|
+
if (!pkg) continue;
|
|
121
|
+
|
|
122
|
+
enqueueResolvedDeps(pkg.dependencies, names, queue, useVersion9, seen);
|
|
123
|
+
enqueueResolvedDeps(
|
|
124
|
+
pkg.optionalDependencies,
|
|
125
|
+
names,
|
|
126
|
+
queue,
|
|
127
|
+
useVersion9,
|
|
128
|
+
seen,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Peer requirement values are name → semver-range, not resolved depPaths.
|
|
133
|
+
* Just record the names so a patch on a peer-only external transitive
|
|
134
|
+
* survives filtering (mirrors the bun walker and the sister manifest
|
|
135
|
+
* walker, which both include peerDependencies).
|
|
136
|
+
*/
|
|
137
|
+
collectNames(pkg.peerDependencies, names);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return names;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log.debug(
|
|
143
|
+
`Failed to walk pnpm lockfile for installed names: ${err instanceof Error ? err.message : String(err)}`,
|
|
144
|
+
);
|
|
145
|
+
return new Set();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Convert a raw importer id (as returned by `getLockfileImporterId` or a
|
|
151
|
+
* package's rootRelativeDir) to the form actually used as a key in
|
|
152
|
+
* `lockfile.importers`: POSIX separators, with the Rush `../../` prefix when
|
|
153
|
+
* the workspace lives under `common/config/rush`. Lockfile keys are always
|
|
154
|
+
* POSIX regardless of the host OS, so backslashes are normalized
|
|
155
|
+
* unconditionally rather than relying on `path.sep`.
|
|
156
|
+
*/
|
|
157
|
+
function toLockfileImporterKey(importerId: string, isRush: boolean): string {
|
|
158
|
+
const posix = importerId
|
|
159
|
+
.split(path.sep)
|
|
160
|
+
.join(path.posix.sep)
|
|
161
|
+
.replace(/\\/g, "/");
|
|
162
|
+
return isRush ? `../../${posix}` : posix;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type ResolvedDeps = Record<string, string>;
|
|
166
|
+
|
|
167
|
+
type PnpmImporter = {
|
|
168
|
+
dependencies?: ResolvedDeps;
|
|
169
|
+
optionalDependencies?: ResolvedDeps;
|
|
170
|
+
devDependencies?: ResolvedDeps;
|
|
171
|
+
peerDependencies?: ResolvedDeps;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
type PnpmPackage = {
|
|
175
|
+
dependencies?: ResolvedDeps;
|
|
176
|
+
optionalDependencies?: ResolvedDeps;
|
|
177
|
+
peerDependencies?: ResolvedDeps;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
function enqueueImporterDeps({
|
|
181
|
+
importer,
|
|
182
|
+
names,
|
|
183
|
+
queue,
|
|
184
|
+
useVersion9,
|
|
185
|
+
includeDevDependencies,
|
|
186
|
+
}: {
|
|
187
|
+
importer: PnpmImporter;
|
|
188
|
+
names: Set<string>;
|
|
189
|
+
queue: string[];
|
|
190
|
+
useVersion9: boolean;
|
|
191
|
+
includeDevDependencies: boolean;
|
|
192
|
+
}): void {
|
|
193
|
+
enqueueResolvedDeps(importer.dependencies, names, queue, useVersion9);
|
|
194
|
+
enqueueResolvedDeps(importer.optionalDependencies, names, queue, useVersion9);
|
|
195
|
+
if (includeDevDependencies) {
|
|
196
|
+
enqueueResolvedDeps(importer.devDependencies, names, queue, useVersion9);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Importer peerDependencies usually aren't a separate map in the lockfile
|
|
200
|
+
* (autoInstallPeers folds them into `dependencies`), but record names if
|
|
201
|
+
* they happen to be present.
|
|
202
|
+
*/
|
|
203
|
+
collectNames(importer.peerDependencies, names);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function enqueueResolvedDeps(
|
|
207
|
+
deps: ResolvedDeps | undefined,
|
|
208
|
+
names: Set<string>,
|
|
209
|
+
queue: string[],
|
|
210
|
+
useVersion9: boolean,
|
|
211
|
+
seen?: Set<string>,
|
|
212
|
+
): void {
|
|
213
|
+
if (!deps) return;
|
|
214
|
+
|
|
215
|
+
for (const [alias, ref] of Object.entries(deps)) {
|
|
216
|
+
/**
|
|
217
|
+
* The alias is the name as listed in the parent's dependencies map. For
|
|
218
|
+
* non-aliased installs this is also the resolved package name. We add it
|
|
219
|
+
* to the set as a candidate name; visiting the actual depPath below
|
|
220
|
+
* refines this with the true installed name.
|
|
221
|
+
*/
|
|
222
|
+
names.add(alias);
|
|
223
|
+
|
|
224
|
+
const depPath = refToRelative(ref, alias, useVersion9);
|
|
225
|
+
if (depPath && !seen?.has(depPath)) {
|
|
226
|
+
queue.push(depPath);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function collectNames(
|
|
232
|
+
deps: ResolvedDeps | undefined,
|
|
233
|
+
names: Set<string>,
|
|
234
|
+
): void {
|
|
235
|
+
if (!deps) return;
|
|
236
|
+
for (const name of Object.keys(deps)) {
|
|
237
|
+
names.add(name);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Mirrors `@pnpm/dependency-path`'s `refToRelative`. The depPath shape differs
|
|
243
|
+
* between pnpm 8 (lockfile v6, normalized to v5 keys like `/foo/1.0.0`) and
|
|
244
|
+
* pnpm 9 (lockfile v9 keys like `foo@1.0.0`). Returns the depPath used as a
|
|
245
|
+
* key in `lockfile.packages`, or null if the ref points to a workspace link.
|
|
246
|
+
*/
|
|
247
|
+
function refToRelative(
|
|
248
|
+
reference: string,
|
|
249
|
+
pkgName: string,
|
|
250
|
+
useVersion9: boolean,
|
|
251
|
+
): string | null {
|
|
252
|
+
if (!reference) return null;
|
|
253
|
+
if (reference.startsWith("link:")) return null;
|
|
254
|
+
return useVersion9
|
|
255
|
+
? refToRelativeV9(reference, pkgName)
|
|
256
|
+
: refToRelativeV8(reference, pkgName);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function refToRelativeV9(reference: string, pkgName: string): string | null {
|
|
260
|
+
if (reference.startsWith("@")) return reference;
|
|
261
|
+
const atIndex = reference.indexOf("@");
|
|
262
|
+
if (atIndex === -1) return `${pkgName}@${reference}`;
|
|
263
|
+
const colonIndex = reference.indexOf(":");
|
|
264
|
+
const bracketIndex = reference.indexOf("(");
|
|
265
|
+
if (
|
|
266
|
+
(colonIndex === -1 || atIndex < colonIndex) &&
|
|
267
|
+
(bracketIndex === -1 || atIndex < bracketIndex)
|
|
268
|
+
) {
|
|
269
|
+
return reference;
|
|
270
|
+
}
|
|
271
|
+
return `${pkgName}@${reference}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* v8 form: pnpm 8 (lockfile v6) is normalized on read to v5-style depPaths
|
|
276
|
+
* with leading slash and `/` separator between name and version. Plain
|
|
277
|
+
* version refs build that key; refs already containing a `/` (peer-suffixed
|
|
278
|
+
* or pre-formed) are returned verbatim. Mirrors `@pnpm/dependency-path@2.x`.
|
|
279
|
+
*/
|
|
280
|
+
function refToRelativeV8(reference: string, pkgName: string): string | null {
|
|
281
|
+
if (reference.startsWith("file:")) return reference;
|
|
282
|
+
const slashIndex = reference.indexOf("/");
|
|
283
|
+
const bracketIndex = reference.indexOf("(");
|
|
284
|
+
const noSlashBeforeBracket =
|
|
285
|
+
bracketIndex !== -1 && reference.lastIndexOf("/", bracketIndex) === -1;
|
|
286
|
+
if (slashIndex === -1 || noSlashBeforeBracket) {
|
|
287
|
+
return `/${pkgName}/${reference}`;
|
|
288
|
+
}
|
|
289
|
+
return reference;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Extract the bare package name from a pnpm depPath. Strips the optional
|
|
294
|
+
* peer-resolution suffix (e.g. `(react@18.0.0)`) before parsing. Handles
|
|
295
|
+
* both v9 (`@scope/foo@1.0.0`) and v8 (`/@scope/foo/1.0.0`) shapes.
|
|
296
|
+
*/
|
|
297
|
+
function extractPackageName(depPath: string): string {
|
|
298
|
+
const peerStart = indexOfPeersSuffix(depPath);
|
|
299
|
+
const trimmed = peerStart === -1 ? depPath : depPath.substring(0, peerStart);
|
|
300
|
+
|
|
301
|
+
if (trimmed.startsWith("/")) {
|
|
302
|
+
/** v8 v5-style: `/<name>/<version>` */
|
|
303
|
+
const stripped = trimmed.slice(1);
|
|
304
|
+
if (stripped.startsWith("@")) {
|
|
305
|
+
const secondSlash = stripped.indexOf("/", stripped.indexOf("/") + 1);
|
|
306
|
+
return secondSlash === -1 ? stripped : stripped.slice(0, secondSlash);
|
|
307
|
+
}
|
|
308
|
+
const firstSlash = stripped.indexOf("/");
|
|
309
|
+
return firstSlash === -1 ? stripped : stripped.slice(0, firstSlash);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return getPackageName(trimmed);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Mirrors `@pnpm/dependency-path`'s `indexOfPeersSuffix`. Returns the index
|
|
317
|
+
* where the peer-resolution suffix starts, or -1 if there is none.
|
|
318
|
+
*/
|
|
319
|
+
function indexOfPeersSuffix(depPath: string): number {
|
|
320
|
+
if (!depPath.endsWith(")")) return -1;
|
|
321
|
+
let open = 1;
|
|
322
|
+
for (let i = depPath.length - 2; i >= 0; i--) {
|
|
323
|
+
if (depPath[i] === "(") {
|
|
324
|
+
open--;
|
|
325
|
+
} else if (depPath[i] === ")") {
|
|
326
|
+
open++;
|
|
327
|
+
} else if (!open) {
|
|
328
|
+
return i + 1;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return -1;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Fallback when the lockfile is missing `packages`: just return importer
|
|
336
|
+
* direct dep names so we at least cover some of the graph.
|
|
337
|
+
*/
|
|
338
|
+
function collectImporterDirectNames(
|
|
339
|
+
importers: Record<string, PnpmImporter>,
|
|
340
|
+
importerIds: string[],
|
|
341
|
+
targetImporterId: string,
|
|
342
|
+
includeDevDependencies: boolean,
|
|
343
|
+
): Set<string> {
|
|
344
|
+
const names = new Set<string>();
|
|
345
|
+
for (const importerId of importerIds) {
|
|
346
|
+
const importer = importers[importerId];
|
|
347
|
+
if (!importer) continue;
|
|
348
|
+
const isTarget = importerId === targetImporterId;
|
|
349
|
+
for (const name of Object.keys(importer.dependencies ?? {}))
|
|
350
|
+
names.add(name);
|
|
351
|
+
for (const name of Object.keys(importer.optionalDependencies ?? {})) {
|
|
352
|
+
names.add(name);
|
|
353
|
+
}
|
|
354
|
+
for (const name of Object.keys(importer.peerDependencies ?? {})) {
|
|
355
|
+
names.add(name);
|
|
356
|
+
}
|
|
357
|
+
if (isTarget && includeDevDependencies) {
|
|
358
|
+
for (const name of Object.keys(importer.devDependencies ?? {})) {
|
|
359
|
+
names.add(name);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return names;
|
|
364
|
+
}
|