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.
@@ -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
+ }