From 9599698c56e5227dfc93c2f40cebfbca9b433bba Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:39:14 -0700 Subject: [PATCH 01/17] Add edge case for projects with duplicate names --- apps/lockfile-explorer/.vscode/launch.json | 2 +- .../lfxGraph-edge-cases-v5.4.test.ts.snap | 123 ++++++++++++++++++ .../fixtures/edge-cases/pnpm-lock-v5.4.yaml | 54 ++++++++ .../fixtures/edge-cases/website-sample-1.md | 4 + .../test/lfxGraph-edge-cases-v5.4.test.ts | 22 ++++ 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap create mode 100644 apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml create mode 100644 apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/website-sample-1.md create mode 100644 apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts diff --git a/apps/lockfile-explorer/.vscode/launch.json b/apps/lockfile-explorer/.vscode/launch.json index 9cc2fd9f58d..b1a4e7ccc89 100644 --- a/apps/lockfile-explorer/.vscode/launch.json +++ b/apps/lockfile-explorer/.vscode/launch.json @@ -20,7 +20,7 @@ "name": "Single Jest test", "program": "${workspaceFolder}/node_modules/@rushstack/heft/lib/start.js", "cwd": "${workspaceFolder}", - "args": ["--debug", "test", "--clean", "-u", "--test-path-pattern", "lfxGraphLoader60"], + "args": ["--debug", "test", "--clean", "-u", "--test-path-pattern", "edge-cases"], "console": "integratedTerminal", "sourceMaps": true }, diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap new file mode 100644 index 00000000000..1be82a27f6d --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap @@ -0,0 +1,123 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` +"entries: + - dependencies: + - dependencyType: regular + entryId: /color/5.0.2 + name: color + peerDependencyMeta: {} + resolvedEntryJsonId: 5 + version: 5.0.2 + displayText: 'Project: duplicate (duplicate-1/duplicate)' + entryId: project:./common/temp/undefined/duplicate-1/duplicate + entryPackageName: duplicate (duplicate-1/duplicate) + entryPackageVersion: '' + entrySuffix: '' + jsonId: 0 + kind: 1 + packageJsonFolderPath: ./common/temp/undefined/duplicate-1/duplicate + rawEntryId: duplicate-1/duplicate + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-string/2.1.2 + name: color-string + peerDependencyMeta: {} + resolvedEntryJsonId: 4 + version: 2.1.2 + displayText: 'Project: duplicate (duplicate-2/duplicate)' + entryId: project:./common/temp/undefined/duplicate-2/duplicate + entryPackageName: duplicate (duplicate-2/duplicate) + entryPackageVersion: '' + entrySuffix: '' + jsonId: 1 + kind: 1 + packageJsonFolderPath: ./common/temp/undefined/duplicate-2/duplicate + rawEntryId: duplicate-2/duplicate + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-name/2.0.2 + name: color-name + peerDependencyMeta: {} + resolvedEntryJsonId: 3 + version: 2.0.2 + displayText: color-convert 3.1.2 + entryId: '' + entryPackageName: color-convert + entryPackageVersion: 3.1.2 + entrySuffix: '' + jsonId: 2 + kind: 2 + packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/color-convert@3.1.2/node_modules/color-convert + rawEntryId: /color-convert/3.1.2 + referrerJsonIds: + - 5 + transitivePeerDependencies: [] + - dependencies: [] + displayText: color-name 2.0.2 + entryId: '' + entryPackageName: color-name + entryPackageVersion: 2.0.2 + entrySuffix: '' + jsonId: 3 + kind: 2 + packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/color-name@2.0.2/node_modules/color-name + rawEntryId: /color-name/2.0.2 + referrerJsonIds: + - 2 + - 4 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-name/2.0.2 + name: color-name + peerDependencyMeta: {} + resolvedEntryJsonId: 3 + version: 2.0.2 + displayText: color-string 2.1.2 + entryId: '' + entryPackageName: color-string + entryPackageVersion: 2.1.2 + entrySuffix: '' + jsonId: 4 + kind: 2 + packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/color-string@2.1.2/node_modules/color-string + rawEntryId: /color-string/2.1.2 + referrerJsonIds: + - 1 + - 5 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-convert/3.1.2 + name: color-convert + peerDependencyMeta: {} + resolvedEntryJsonId: 2 + version: 3.1.2 + - dependencyType: regular + entryId: /color-string/2.1.2 + name: color-string + peerDependencyMeta: {} + resolvedEntryJsonId: 4 + version: 2.1.2 + displayText: color 5.0.2 + entryId: '' + entryPackageName: color + entryPackageVersion: 5.0.2 + entrySuffix: '' + jsonId: 5 + kind: 2 + packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/color@5.0.2/node_modules/color + rawEntryId: /color/5.0.2 + referrerJsonIds: + - 0 + transitivePeerDependencies: [] +workspace: + pnpmLockfilePath: common/temp/pnpm-lock.yaml + workspaceRootFolder: /repo +" +`; diff --git a/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml new file mode 100644 index 00000000000..da1986e1571 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml @@ -0,0 +1,54 @@ +lockfileVersion: 5.4 + +importers: + duplicate-1/duplicate: + specifiers: + color: ^5.0.2 + dependencies: + color: 5.0.2 + + duplicate-2/duplicate: + specifiers: + color-string: ^2.1.2 + dependencies: + color-string: 2.1.2 + +packages: + /color-convert/3.1.2: + resolution: + { + integrity: sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg== + } + engines: { node: '>=14.6' } + dependencies: + color-name: 2.0.2 + dev: false + + /color-name/2.0.2: + resolution: + { + integrity: sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A== + } + engines: { node: '>=12.20' } + dev: false + + /color-string/2.1.2: + resolution: + { + integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA== + } + engines: { node: '>=18' } + dependencies: + color-name: 2.0.2 + dev: false + + /color/5.0.2: + resolution: + { + integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA== + } + engines: { node: '>=18' } + dependencies: + color-convert: 3.1.2 + color-string: 2.1.2 + dev: false diff --git a/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/website-sample-1.md b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/website-sample-1.md new file mode 100644 index 00000000000..ff328176fab --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/website-sample-1.md @@ -0,0 +1,4 @@ +# fixtures/edge-cases + +This test fixture is a PNPM workspace crafted to reproduce interesting edge cases in the `lfxGraphLoader` algorithm. + diff --git a/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts new file mode 100644 index 00000000000..fe9bae9ce9d --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; + +import * as graphTestHelpers from './graphTestHelpers'; + +export const workspace: IJsonLfxWorkspace = { + workspaceRootFolder: '/repo', + pnpmLockfilePath: 'pnpm-lock.yaml', + rushConfig: undefined +}; + +describe('lfxGraph-edge-cases-v5.4', () => { + it('loads a workspace', async () => { + const serializedYaml: string = await graphTestHelpers.loadAndSerializeLFxGraphAsync({ + lockfilePathUnderFixtures: '/edge-cases/pnpm-lock-v5.4.yaml', + workspace: workspace + }); + expect(serializedYaml).toMatchSnapshot(); + }); +}); From 6ba8da4ad35084d24dbae59a44cfe0de5cf313b8 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:08:19 -0700 Subject: [PATCH 02/17] Replace custom interfaces with official PNPM types; temporarily remove the V6 kludge --- apps/lockfile-explorer/package.json | 5 +- .../cli/explorer/ExplorerCommandLineParser.ts | 5 +- .../src/cli/lint/actions/CheckAction.ts | 19 +-- .../src/graph/lfxGraphLoader.ts | 108 +++--------------- .../src/graph/test/graphTestHelpers.ts | 2 +- .../rush/browser-approved-packages.json | 4 + .../config/subspaces/default/pnpm-lock.yaml | 26 ++++- .../config/subspaces/default/repo-state.json | 2 +- 8 files changed, 55 insertions(+), 116 deletions(-) diff --git a/apps/lockfile-explorer/package.json b/apps/lockfile-explorer/package.json index 4e175cb7851..2b5eae2114f 100644 --- a/apps/lockfile-explorer/package.json +++ b/apps/lockfile-explorer/package.json @@ -39,7 +39,7 @@ "_phase:test": "heft run --only test -- --clean" }, "peerDependencies": { - "@types/express": "^4.17.21" + "@types/express": "^5.0.3" }, "peerDependenciesMeta": { "@types/express": { @@ -55,7 +55,8 @@ "@types/update-notifier": "~6.0.1", "eslint": "~9.25.1", "local-node-rig": "workspace:*", - "@pnpm/lockfile-types": "^5.1.5", + "@pnpm/lockfile-types": "7.1.3", + "@pnpm/types": "1000.8.0", "@types/semver": "7.5.0" }, "dependencies": { diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index cc40653e354..c4012c14b9e 100644 --- a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -15,7 +15,6 @@ import { CommandLineParser, type IRequiredCommandLineStringParameter } from '@rushstack/ts-command-line'; -import type { Lockfile } from '@pnpm/lockfile-types'; import { type LfxGraph, lfxGraphSerializer, @@ -152,11 +151,11 @@ export class ExplorerCommandLineParser extends CommandLineParser { app.get('/api/graph', async (req: express.Request, res: express.Response) => { const pnpmLockfileText: string = await FileSystem.readFileAsync(appState.pnpmLockfileLocation); - const lockfile: Lockfile = yaml.load(pnpmLockfileText) as Lockfile; + const lockfile: unknown = yaml.load(pnpmLockfileText) as unknown; const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph( appState.lfxWorkspace, - lockfile as lfxGraphLoader.ILockfilePackageType, + lockfile, appState.lfxWorkspace.rushConfig?.subspaceName ?? '' ); diff --git a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts index 389bb27001b..6709a462796 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts @@ -7,6 +7,8 @@ import { RushConfiguration, type RushConfigurationProject, type Subspace } from import path from 'path'; import yaml from 'js-yaml'; import semver from 'semver'; +import type * as lockfileTypes from '@pnpm/lockfile-types'; +import type * as pnpmTypes from '@pnpm/types'; import { AlreadyReportedError, Async, FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library'; import lockfileLintSchema from '../../../schemas/lockfile-lint.schema.json'; @@ -17,7 +19,6 @@ import { parseDependencyPath, splicePackageWithVersion } from '../../../utils/shrinkwrap'; -import type { Lockfile, LockfileV6 } from '@pnpm/lockfile-types'; export interface ILintRule { rule: 'restrict-versions'; @@ -40,7 +41,7 @@ export class CheckAction extends CommandLineAction { private _rushConfiguration!: RushConfiguration; private _checkedProjects: Set; - private _docMap: Map; + private _docMap: Map; public constructor(parser: LintCommandLineParser) { super({ @@ -59,8 +60,8 @@ export class CheckAction extends CommandLineAction { private async _checkVersionCompatibilityAsync( shrinkwrapFileMajorVersion: number, - packages: Lockfile['packages'], - dependencyPath: string, + packages: lockfileTypes.PackageSnapshots | undefined, + dependencyPath: pnpmTypes.DepPath, requiredVersions: Record, checkedDependencyPaths: Set ): Promise { @@ -84,7 +85,7 @@ export class CheckAction extends CommandLineAction { shrinkwrapFileMajorVersion, dependencyPackageName, dependencyPackageVersion - ), + ) as pnpmTypes.DepPath, requiredVersions, checkedDependencyPaths ); @@ -103,12 +104,12 @@ export class CheckAction extends CommandLineAction { const projectFolder: string = project.projectFolder; const subspace: Subspace = project.subspace; const shrinkwrapFilename: string = subspace.getCommittedShrinkwrapFilePath(); - let doc: Lockfile | LockfileV6; + let doc: lockfileTypes.Lockfile; if (this._docMap.has(shrinkwrapFilename)) { doc = this._docMap.get(shrinkwrapFilename)!; } else { const pnpmLockfileText: string = await FileSystem.readFileAsync(shrinkwrapFilename); - doc = yaml.load(pnpmLockfileText) as Lockfile | LockfileV6; + doc = yaml.load(pnpmLockfileText) as lockfileTypes.Lockfile; this._docMap.set(shrinkwrapFilename, doc); } const { importers, lockfileVersion, packages } = doc; @@ -120,7 +121,7 @@ export class CheckAction extends CommandLineAction { if (path.resolve(projectFolder, relativePath) === projectFolder) { const dependenciesEntries: [string, unknown][] = Object.entries(dependencies ?? {}); for (const [dependencyName, dependencyValue] of dependenciesEntries) { - const fullDependencyPath: string = splicePackageWithVersion( + const fullDependencyPath: pnpmTypes.DepPath = splicePackageWithVersion( shrinkwrapFileMajorVersion, dependencyName, typeof dependencyValue === 'string' @@ -131,7 +132,7 @@ export class CheckAction extends CommandLineAction { specifier: string; } ).version - ); + ) as pnpmTypes.DepPath; if (fullDependencyPath.includes('link:')) { const dependencyProject: RushConfigurationProject | undefined = this._rushConfiguration.getProjectByName(dependencyName); diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index 64ed34f997a..48dda0056f1 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -2,6 +2,8 @@ // See LICENSE in the project root for license information. import { Path } from '@lifaon/path'; +import type * as lockfileTypes from '@pnpm/lockfile-types'; +import type * as pnpmTypes from '@pnpm/types'; import { type ILfxGraphDependencyOptions, @@ -21,60 +23,6 @@ enum PnpmLockfileVersion { V5 } -export interface ILockfileImporterV6 { - dependencies?: { - [key: string]: { - specifier: string; - version: string; - }; - }; - devDependencies?: { - [key: string]: { - specifier: string; - version: string; - }; - }; -} -export interface ILockfileImporterV5 { - specifiers?: Record; - dependencies?: Record; - devDependencies?: Record; -} -export interface ILockfilePackageType { - lockfileVersion: number | string; - importers?: { - [key: string]: ILockfileImporterV5 | ILockfileImporterV6; - }; - packages?: { - [key: string]: { - resolution: { - integrity: string; - }; - dependencies?: Record; - peerDependencies?: Record; - dev: boolean; - }; - }; -} - -export interface ILockfileNode { - dependencies?: { - [key: string]: string; - }; - devDependencies?: { - [key: string]: string; - }; - peerDependencies?: { - [key: string]: string; - }; - peerDependenciesMeta?: { - [key: string]: { - optional: boolean; - }; - }; - transitivePeerDependencies?: string[]; -} - const packageEntryIdRegex: RegExp = new RegExp('/(.*)/([^/]+)$'); function createLockfileDependency( @@ -82,7 +30,7 @@ function createLockfileDependency( version: string, dependencyType: LfxDependencyKind, containingEntry: LfxGraphEntry, - node?: ILockfileNode + node?: lockfileTypes.PackageSnapshot ): LfxGraphDependency { const result: ILfxGraphDependencyOptions = { name, @@ -129,8 +77,10 @@ function createLockfileDependency( function parseDependencies( dependencies: LfxGraphDependency[], lockfileEntry: LfxGraphEntry, - node: ILockfileNode + either: lockfileTypes.ProjectSnapshot | lockfileTypes.PackageSnapshot ): void { + const node: lockfileTypes.ProjectSnapshot & lockfileTypes.PackageSnapshot = + either as unknown as lockfileTypes.ProjectSnapshot & lockfileTypes.PackageSnapshot; if (node.dependencies) { for (const [pkgName, pkgVersion] of Object.entries(node.dependencies)) { dependencies.push( @@ -160,7 +110,7 @@ function parseDependencies( function createLockfileEntry(options: { rawEntryId: string; kind: LfxGraphEntryKind; - rawYamlData: ILockfileNode; + rawYamlData: lockfileTypes.PackageSnapshot | lockfileTypes.ProjectSnapshot; duplicates?: Set; subspaceName?: string; }): LfxGraphEntry { @@ -255,37 +205,6 @@ function createLockfileEntry(options: { return lockfileEntry; } -/** - * Transform any newer lockfile formats to the following format: - * [packageName]: - * specifier: ... - * version: ... - */ -function getImporterValue( - importerValue: ILockfileImporterV5 | ILockfileImporterV6, - pnpmLockfileVersion: PnpmLockfileVersion -): ILockfileImporterV5 { - if (pnpmLockfileVersion === PnpmLockfileVersion.V6) { - const v6ImporterValue: ILockfileImporterV6 = importerValue as ILockfileImporterV6; - const v5ImporterValue: ILockfileImporterV5 = { - specifiers: {}, - dependencies: {}, - devDependencies: {} - }; - for (const [depName, depDetails] of Object.entries(v6ImporterValue.dependencies ?? {})) { - v5ImporterValue.specifiers![depName] = depDetails.specifier; - v5ImporterValue.dependencies![depName] = depDetails.version; - } - for (const [depName, depDetails] of Object.entries(v6ImporterValue.devDependencies ?? {})) { - v5ImporterValue.specifiers![depName] = depDetails.specifier; - v5ImporterValue.devDependencies![depName] = depDetails.version; - } - return v5ImporterValue; - } else { - return importerValue as ILockfileImporterV5; - } -} - /** * Parse through the lockfile and create all the corresponding LockfileEntries and LockfileDependencies * to construct the lockfile graph. @@ -294,18 +213,19 @@ function getImporterValue( */ export function generateLockfileGraph( workspace: IJsonLfxWorkspace, - lockfile: ILockfilePackageType, + lockfileJson: unknown, subspaceName?: string ): LfxGraph { + const lockfile: lockfileTypes.Lockfile = lockfileJson as lockfileTypes.Lockfile; let pnpmLockfileVersion: PnpmLockfileVersion = PnpmLockfileVersion.V5; if (parseInt(lockfile.lockfileVersion.toString(), 10) === 6) { pnpmLockfileVersion = PnpmLockfileVersion.V6; } if (lockfile.packages && pnpmLockfileVersion === PnpmLockfileVersion.V6) { - const updatedPackages: ILockfilePackageType['packages'] = {}; + const updatedPackages: lockfileTypes.PackageSnapshots = {}; for (const [dependencyPath, dependency] of Object.entries(lockfile.packages)) { - updatedPackages[convertLockfileV6DepPathToV5DepPath(dependencyPath)] = dependency; + updatedPackages[convertLockfileV6DepPathToV5DepPath(dependencyPath) as pnpmTypes.DepPath] = dependency; } lockfile.packages = updatedPackages; } @@ -337,7 +257,7 @@ export function generateLockfileGraph( // entryId: normalizedPath, rawEntryId: importerKey, kind: LfxGraphEntryKind.Project, - rawYamlData: getImporterValue(importerValue, pnpmLockfileVersion), + rawYamlData: importerValue, duplicates, subspaceName }); @@ -349,14 +269,14 @@ export function generateLockfileGraph( const allPackages: LfxGraphEntry[] = []; if (lockfile.packages) { - for (const [dependencyKey, dependencyValue] of Object.entries(lockfile.packages)) { + for (const [dependencyKey, dependencyValue] of Object.entries(lockfile.packages ?? {})) { // const normalizedPath = new Path(dependencyKey).makeAbsolute('/').toString(); const currEntry: LfxGraphEntry = createLockfileEntry({ // entryId: normalizedPath, rawEntryId: dependencyKey, kind: LfxGraphEntryKind.Package, - rawYamlData: dependencyValue, + rawYamlData: dependencyValue as lockfileTypes.PackageSnapshot, subspaceName }); diff --git a/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts b/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts index 52583dec6c2..138475b40b0 100644 --- a/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts +++ b/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts @@ -23,7 +23,7 @@ export async function loadAndSerializeLFxGraphAsync(options: { FIXTURES_FOLDER + options.lockfilePathUnderFixtures, { convertLineEndings: NewlineKind.Lf } ); - const lockfileObject = yaml.load(lockfileYaml) as lfxGraphLoader.ILockfilePackageType; + const lockfileObject = yaml.load(lockfileYaml); const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(options.workspace, lockfileObject); const serializedObject: IJsonLfxGraph = lfxGraphSerializer.serializeToJson(graph); const serializedYaml: string = yaml.dump(serializedObject, { diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 15e248ddeba..bf68568ed36 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -14,6 +14,10 @@ "name": "@lifaon/path", "allowedCategories": [ "libraries" ] }, + { + "name": "@pnpm/types", + "allowedCategories": [ "libraries" ] + }, { "name": "@radix-ui/colors", "allowedCategories": [ "libraries" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index b19882175df..fd8cdf97b46 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -237,8 +237,11 @@ importers: version: 5.1.0 devDependencies: '@pnpm/lockfile-types': - specifier: ^5.1.5 - version: 5.1.5 + specifier: 7.1.3 + version: 7.1.3 + '@pnpm/types': + specifier: 1000.8.0 + version: 1000.8.0 '@rushstack/heft': specifier: workspace:* version: link:../heft @@ -10491,11 +10494,11 @@ packages: ramda: 0.27.2 dev: false - /@pnpm/lockfile-types@5.1.5: - resolution: {integrity: sha512-02FP0HynzX+2DcuPtuMy7PH+kLIC0pevAydAOK+zug2bwdlSLErlvSkc+4+3dw60eRWgUXUqyfO2eR/Ansdbng==} - engines: {node: '>=16.14'} + /@pnpm/lockfile-types@7.1.3: + resolution: {integrity: sha512-ifMGKjiBFweZiI9nRYw+N4oP42tDaxHGzla/fuEqn8rOLzITJKfD9G2KfF7u8ZfZyW9lHFL/FsN9t1+q6L+qyQ==} + engines: {node: '>=18.12'} dependencies: - '@pnpm/types': 9.4.2 + '@pnpm/types': 11.1.0 dev: true /@pnpm/lockfile.types@1.0.3: @@ -10566,6 +10569,16 @@ packages: engines: {node: '>=18.12'} dev: false + /@pnpm/types@1000.8.0: + resolution: {integrity: sha512-yx86CGHHquWAI0GgKIuV/RnYewcf5fVFZemC45C/K2cX0uV8GB8TUP541ZrokWola2fZx5sn1vL7xzbceRZfoQ==} + engines: {node: '>=18.12'} + dev: true + + /@pnpm/types@11.1.0: + resolution: {integrity: sha512-wnlOhu7hjv9/qsf2cbK0YqpaV9c4LS69Utxd+r8hq/GWhyrOHcM1QOlfQb0Mzci0q4DDgB8VXT4dhBnEBL4c5g==} + engines: {node: '>=18.12'} + dev: true + /@pnpm/types@12.2.0: resolution: {integrity: sha512-5RtwWhX39j89/Tmyv2QSlpiNjErA357T/8r1Dkg+2lD3P7RuS7Xi2tChvmOC3VlezEFNcWnEGCOeKoGRkDuqFA==} engines: {node: '>=18.12'} @@ -10583,6 +10596,7 @@ packages: /@pnpm/types@9.4.2: resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} engines: {node: '>=16.14'} + dev: false /@pnpm/write-project-manifest@1.1.7: resolution: {integrity: sha512-OLkDZSqkA1mkoPNPvLFXyI6fb0enCuFji6Zfditi/CLAo9kmIhQFmEUDu4krSB8i908EljG8YwL5Xjxzm5wsWA==} diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 3c852d397ea..655041c65b6 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "43e8674ca74b9c3f20cf12f03de5ce2968017331", + "pnpmShrinkwrapHash": "38797dd39684687f2ae92bb0661933fe861efc98", "preferredVersionsHash": "61cd419c533464b580f653eb5f5a7e27fe7055ca" } From b56fb78ec277b78dc32bc691dba86961750805bf Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 01:04:28 -0700 Subject: [PATCH 03/17] Add lockfilePath.ts utility that can completely eliminate `@lifaon/path` --- .../src/graph/lockfilePath.ts | 122 ++++++++++++++++++ .../src/graph/test/lockfilePath.test.ts | 57 ++++++++ 2 files changed, 179 insertions(+) create mode 100644 apps/lockfile-explorer/src/graph/lockfilePath.ts create mode 100644 apps/lockfile-explorer/src/graph/test/lockfilePath.test.ts diff --git a/apps/lockfile-explorer/src/graph/lockfilePath.ts b/apps/lockfile-explorer/src/graph/lockfilePath.ts new file mode 100644 index 00000000000..1a03e4c58f3 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/lockfilePath.ts @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * For example, retrieves `d` from `/a/b/c/d`. + */ +export function getBaseNameOf(importerPath: string): string { + if (importerPath.length === 0) { + return ''; + } + + const index: number = importerPath.lastIndexOf('/'); + if (index === importerPath.length - 1) { + throw new Error('Error: Path has a trailing slash'); + } + if (index >= 0) { + return importerPath.substring(index + 1); + } + return importerPath; +} + +/** + * For example, retrieves `/a/b/c` from `/a/b/c/d`. + */ +export function getParentOf(importerPath: string): string { + if (importerPath === '' || importerPath === '.' || importerPath === '/') { + throw new Error('Error: Path has no parent'); + } + + const index: number = importerPath.lastIndexOf('/'); + if (index === importerPath.length - 1) { + throw new Error('Error: Path has a trailing slash'); + } + if (index === 0) { + return '/'; + } + if (index < 0) { + return '.'; + } + return importerPath.substring(0, index); +} + +/** + * Cheaply resolves a relative path against a base path, assuming the paths are delimited by `/`, + * and assuming the basePath is already in normal form. An error occurs if the relative path + * goes above the root folder. + * + * @example + * ```ts + * getAbsolutePath(`a/b/c`, `d/e`) === `a/b/c/d/e` + * getAbsolutePath(`/a/b/c`, `d/e`) === `/a/b/c/d/e` + * getAbsolutePath(`/a/b/c`, `/d/e`) === `/d/e` + * getAbsolutePath(`a/b/c`, `../../f`) === `a/f` + * getAbsolutePath(`a/b/c`, `.././/f`) === `a/b/f` + * getAbsolutePath(`a/b/c`, `../../..`) === `.` + * getAbsolutePath(`C:/a/b`, `../d`) === `C:/a/d` + * getAbsolutePath(`a/b/c`, `../../../..`) === ERROR + * + * // Degenerate cases: + * getAbsolutePath(`a/b/c/`, `d/`) === `a/b/c/d` // trailing slashes are discarded + * getAbsolutePath(`./../c`, `d`) === `./../c/d` // basePath assumed to be normal form + * getAbsolutePath(`C:\\`, `\\a`) === `C:\\/\\a` // backslashes not supported + * ``` + */ +export function getAbsolute(basePath: string, relativePath: string): string { + let leadingSlash: boolean; + let stack: string[]; + + // Discard intermediary slashes + const relativeParts: string[] = relativePath.split('/').filter((part: string) => part.length > 0); + if (relativePath.startsWith('/')) { + stack = []; + leadingSlash = true; + } else { + // Discard intermediary slashes + stack = basePath.split('/').filter((part: string) => part.length > 0); + leadingSlash = basePath.startsWith('/'); + } + + for (const part of relativeParts) { + if (part === '.') { + // current directory, do nothing + continue; + } else if (part === '..') { + if (stack.length === 0) { + throw new Error('getAbsolutePath(): relativePath goes above the root folder'); + } + stack.pop(); + } else { + stack.push(part); + } + } + if (leadingSlash) { + return '/' + stack.join('/'); + } else { + return stack.length === 0 ? '.' : stack.join('/'); + } +} + +/** + * Returns the two parts joined by exactly one `/`, assuming the parts are already + * in normalized form. The `/` is not added if either part is an empty string. + */ +export function join(leftPart: string, rightPart: string): string { + if (leftPart.length === 0) { + return rightPart; + } + if (rightPart.length === 0) { + return leftPart; + } + + const leftEndsWithSlash: boolean = leftPart[leftPart.length - 1] === '/'; + const rightStartsWithSlash: boolean = rightPart[0] === '/'; + + if (leftEndsWithSlash && rightStartsWithSlash) { + return leftPart + rightPart.substring(1); + } + if (leftEndsWithSlash || rightStartsWithSlash) { + return leftPart + rightPart; + } + return leftPart + '/' + rightPart; +} diff --git a/apps/lockfile-explorer/src/graph/test/lockfilePath.test.ts b/apps/lockfile-explorer/src/graph/test/lockfilePath.test.ts new file mode 100644 index 00000000000..008e3b848f1 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/lockfilePath.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as lockfilePath from '../lockfilePath'; + +describe('lockfilePath', () => { + it('getBaseNameOf', () => { + expect(lockfilePath.getBaseNameOf('/a/b/c/d')).toBe('d'); + expect(lockfilePath.getBaseNameOf('.')).toBe('.'); + expect(lockfilePath.getBaseNameOf('')).toBe(''); + + expect(() => lockfilePath.getParentOf('/a/')).toThrowError('has a trailing slash'); + }); + + it('getParentOf', () => { + expect(lockfilePath.getParentOf('a/b/c/d')).toBe('a/b/c'); + expect(lockfilePath.getParentOf('/a/b/c')).toBe('/a/b'); + expect(lockfilePath.getParentOf('/a/b')).toBe('/a'); + expect(lockfilePath.getParentOf('/a')).toBe('/'); + expect(lockfilePath.getParentOf('a')).toBe('.'); + + expect(() => lockfilePath.getParentOf('')).toThrowError('has no parent'); + expect(() => lockfilePath.getParentOf('/')).toThrowError('has no parent'); + expect(() => lockfilePath.getParentOf('.')).toThrowError('has no parent'); + expect(() => lockfilePath.getParentOf('/a/')).toThrowError('has a trailing slash'); + }); + + it('getAbsolute', () => { + expect(lockfilePath.getAbsolute('a/b/c', 'd/e')).toBe('a/b/c/d/e'); + expect(lockfilePath.getAbsolute('/a/b/c', 'd/e')).toBe('/a/b/c/d/e'); + expect(lockfilePath.getAbsolute('/a/b/c', '/d/e')).toBe('/d/e'); + expect(lockfilePath.getAbsolute('a/b/c', '../../f')).toBe('a/f'); + expect(lockfilePath.getAbsolute('a/b/c', '.././/f')).toBe('a/b/f'); + expect(lockfilePath.getAbsolute('a/b/c', '../../..')).toBe('.'); + expect(lockfilePath.getAbsolute('C:/a/b', '../d')).toBe('C:/a/d'); + + // Error case + expect(() => lockfilePath.getAbsolute('a/b/c', '../../../..')).toThrowError('goes above the root folder'); + + // Degenerate cases + expect(lockfilePath.getAbsolute('a/b/c/', 'd/')).toBe('a/b/c/d'); + expect(lockfilePath.getAbsolute('./../c', 'd')).toBe('./../c/d'); + expect(lockfilePath.getAbsolute('C:\\', '\\a')).toBe('C:\\/\\a'); + }); + + it('join', () => { + expect(lockfilePath.join('', 'a')).toBe('a'); + expect(lockfilePath.join('b', '')).toBe('b'); + expect(lockfilePath.join('a', 'b')).toBe('a/b'); + expect(lockfilePath.join('a/', 'b')).toBe('a/b'); + expect(lockfilePath.join('a', '/b')).toBe('a/b'); + expect(lockfilePath.join('a/', '/b')).toBe('a/b'); + + // Degenerate cases + expect(lockfilePath.join('a//', '/b')).toBe('a//b'); + }); +}); From 6e24583067b592d4ab1d2e1b2d733ab7750af2b6 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 01:55:29 -0700 Subject: [PATCH 04/17] Completed rewrite of 5.4 loader logic --- .../packlets/lfx-shared/IJsonLfxWorkspace.ts | 31 ++- .../src/graph/lfxGraphLoader.ts | 228 ++++++++++-------- .../lfxGraph-edge-cases-v5.4.test.ts.snap | 21 +- ...fxGraph-website-sample-1-v5.4.test.ts.snap | 57 +++-- .../src/graph/test/graphTestHelpers.ts | 2 +- .../test/lfxGraph-edge-cases-v5.4.test.ts | 3 +- .../lfxGraph-website-sample-1-v5.4.test.ts | 3 +- .../src/graph/test/lockfile.test.ts | 4 +- .../src/graph/test/serializeToJson.test.ts | 30 ++- .../src/graph/test/testLockfile.ts | 12 +- apps/lockfile-explorer/src/utils/init.ts | 13 +- 11 files changed, 225 insertions(+), 179 deletions(-) diff --git a/apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts b/apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts index 31c081d1c91..841f95e763d 100644 --- a/apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts +++ b/apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts @@ -3,7 +3,7 @@ export interface IJsonLfxWorkspaceRushConfig { /** - * The rushVersion from rush.json. + * The `rushVersion` field from rush.json. */ readonly rushVersion: string; @@ -16,19 +16,36 @@ export interface IJsonLfxWorkspaceRushConfig { export interface IJsonLfxWorkspace { /** - * Absolute path to the workspace folder that is opened by the app. - * Relative paths are generally relative to this path. + * Absolute path to the workspace folder that is opened by the app, normalized to use forward slashes + * without a trailing slash. + * + * @example `"C:/path/to/MyRepo"` */ - readonly workspaceRootFolder: string; + readonly workspaceRootFullPath: string; /** - * The path to the pnpm-lock.yaml file. + * The path to the "pnpm-lock.yaml" file, relative to `workspaceRootFullPath` + * and normalized to use forward slashes without a leading slash. + * + * @example `"common/temp/my-subspace/pnpm-lock.yaml"` + * @example `"pnpm-lock.yaml"` */ readonly pnpmLockfilePath: string; /** - * If this is a Rush workspace (versus a plain PNPM workspace), then - * this section will be defined. + * The path to the folder of "pnpm-lock.yaml" file, relative to `workspaceRootFullPath` + * and normalized to use forward slashes without a leading slash. + * + * If `pnpm-lack.yaml` is in the `workspaceRootFullPath` folder, then pnpmLockfileFolder + * is the empty string. + * + * @example `"common/temp/my-subspace"` + * @example `""` + */ + readonly pnpmLockfileFolder: string; + + /** + * This section will be defined only if this is a Rush workspace (versus a plain PNPM workspace). */ readonly rushConfig: IJsonLfxWorkspaceRushConfig | undefined; } diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index 48dda0056f1..7db12299704 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { Path } from '@lifaon/path'; import type * as lockfileTypes from '@pnpm/lockfile-types'; import type * as pnpmTypes from '@pnpm/types'; @@ -15,22 +14,17 @@ import { LfxGraphDependency, type IJsonLfxWorkspace } from '../../build/lfx-shared'; +import * as lockfilePath from './lockfilePath'; -import { convertLockfileV6DepPathToV5DepPath } from '../utils/shrinkwrap'; - -enum PnpmLockfileVersion { - V6, - V5 -} - -const packageEntryIdRegex: RegExp = new RegExp('/(.*)/([^/]+)$'); +type PnpmLockfileVersion = 54 | 60 | 90; function createLockfileDependency( name: string, version: string, dependencyType: LfxDependencyKind, containingEntry: LfxGraphEntry, - node?: lockfileTypes.PackageSnapshot + node: Partial | undefined, + workspace: IJsonLfxWorkspace ): LfxGraphDependency { const result: ILfxGraphDependencyOptions = { name, @@ -43,13 +37,10 @@ function createLockfileDependency( if (version.startsWith('link:')) { const relativePath: string = version.substring('link:'.length); - const rootRelativePath: Path | null = new Path('.').relative( - new Path(containingEntry.packageJsonFolderPath).concat(relativePath) + const rootRelativePath: string = lockfilePath.getAbsolute( + containingEntry.packageJsonFolderPath, + relativePath ); - if (!rootRelativePath) { - console.error('No root relative path for dependency!', name); - return new LfxGraphDependency(result); - } result.entryId = 'project:' + rootRelativePath.toString(); } else if (result.version.startsWith('/')) { result.entryId = version; @@ -77,26 +68,43 @@ function createLockfileDependency( function parseDependencies( dependencies: LfxGraphDependency[], lockfileEntry: LfxGraphEntry, - either: lockfileTypes.ProjectSnapshot | lockfileTypes.PackageSnapshot + either: lockfileTypes.ProjectSnapshot | lockfileTypes.PackageSnapshot, + workspace: IJsonLfxWorkspace ): void { - const node: lockfileTypes.ProjectSnapshot & lockfileTypes.PackageSnapshot = - either as unknown as lockfileTypes.ProjectSnapshot & lockfileTypes.PackageSnapshot; + const node: Partial = + either as unknown as Partial; if (node.dependencies) { for (const [pkgName, pkgVersion] of Object.entries(node.dependencies)) { dependencies.push( - createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Regular, lockfileEntry) + createLockfileDependency( + pkgName, + pkgVersion, + LfxDependencyKind.Regular, + lockfileEntry, + undefined, + workspace + ) ); } } if (node.devDependencies) { for (const [pkgName, pkgVersion] of Object.entries(node.devDependencies)) { - dependencies.push(createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Dev, lockfileEntry)); + dependencies.push( + createLockfileDependency( + pkgName, + pkgVersion, + LfxDependencyKind.Dev, + lockfileEntry, + undefined, + workspace + ) + ); } } if (node.peerDependencies) { for (const [pkgName, pkgVersion] of Object.entries(node.peerDependencies)) { dependencies.push( - createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Peer, lockfileEntry, node) + createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Peer, lockfileEntry, node, workspace) ); } } @@ -112,9 +120,10 @@ function createLockfileEntry(options: { kind: LfxGraphEntryKind; rawYamlData: lockfileTypes.PackageSnapshot | lockfileTypes.ProjectSnapshot; duplicates?: Set; - subspaceName?: string; + workspace: IJsonLfxWorkspace; + pnpmLockfileVersion: PnpmLockfileVersion; }): LfxGraphEntry { - const { rawEntryId, kind, rawYamlData, duplicates, subspaceName } = options; + const { rawEntryId, kind, rawYamlData, duplicates, pnpmLockfileVersion, workspace } = options; const result: ILfxGraphEntryOptions = { kind, @@ -129,79 +138,77 @@ function createLockfileEntry(options: { result.rawEntryId = rawEntryId; - if (rawEntryId === '.') { - // Project Root - return new LfxGraphEntry(result); - } + // Example: pnpmLockfilePath = 'common/temp/my-subspace/pnpm-lock.yaml' + // Example: pnpmLockfileFolder = 'common/temp/my-subspace' + const pnpmLockfileFolder: string = workspace.pnpmLockfileFolder; if (kind === LfxGraphEntryKind.Project) { - const rootPackageJsonFolderPath: '' | Path = - new Path(`common/temp/${subspaceName}/package.json`).dirname() || ''; - const packageJsonFolderPath: Path | null = new Path('.').relative( - new Path(rootPackageJsonFolderPath).concat(rawEntryId) - ); - const packageName: string | null = new Path(rawEntryId).basename(); + // Example: rawEntryId = '../../../projects/a' + // Example: packageJsonFolderPath = 'projects/a' + result.packageJsonFolderPath = lockfilePath.getAbsolute(pnpmLockfileFolder, rawEntryId); + result.entryId = 'project:' + result.packageJsonFolderPath; - if (!packageJsonFolderPath || !packageName) { - console.error('Could not construct path for entry: ', rawEntryId); - return new LfxGraphEntry(result); - } + const projectFolderName: string = lockfilePath.getBaseNameOf(rawEntryId); - result.packageJsonFolderPath = packageJsonFolderPath.toString(); - result.entryId = 'project:' + result.packageJsonFolderPath; - result.entryPackageName = packageName.toString(); - if (duplicates?.has(result.entryPackageName)) { - const fullPath: string = new Path(rawEntryId).makeAbsolute('/').toString().substring(1); - result.displayText = `Project: ${result.entryPackageName} (${fullPath})`; - result.entryPackageName = `${result.entryPackageName} (${fullPath})`; + if (!duplicates?.has(projectFolderName)) { + // TODO: The actual package.json name might not match its directory name, + // but we have to load package.json to determine it. + result.entryPackageName = projectFolderName; } else { - result.displayText = 'Project: ' + result.entryPackageName; + result.entryPackageName = `${projectFolderName} (${result.packageJsonFolderPath})`; } + result.displayText = `Project: ${result.entryPackageName}`; } else { result.displayText = rawEntryId; - const match: RegExpExecArray | null = packageEntryIdRegex.exec(rawEntryId); - - if (match) { - const [, packageName, versionPart] = match; + if (pnpmLockfileVersion === 54) { + if (!rawEntryId.startsWith('/')) { + throw new Error('Expecting leading "/" in path: ' + JSON.stringify(rawEntryId)); + } + const lastSlashIndex: number = rawEntryId.lastIndexOf('/'); + if (lastSlashIndex < 0) { + throw new Error('Expecting "/" in path: ' + JSON.stringify(rawEntryId)); + } + const packageName: string = rawEntryId.substring(1, lastSlashIndex); result.entryPackageName = packageName; - const underscoreIndex: number = versionPart.indexOf('_'); - if (underscoreIndex >= 0) { - const version: string = versionPart.substring(0, underscoreIndex); - const suffix: string = versionPart.substring(underscoreIndex + 1); - + // /@rushstack/eslint-config/3.0.1_eslint@8.21.0+typescript@4.7.4 + // --> @rushstack/eslint-config 3.0.1 (eslint@8.21.0+typescript@4.7.4) + const underscoreIndex: number = rawEntryId.indexOf('_', lastSlashIndex); + if (underscoreIndex > 0) { + const version: string = rawEntryId.substring(lastSlashIndex + 1, underscoreIndex); + const suffix: string = rawEntryId.substring(underscoreIndex + 1); + result.displayText = packageName + ' ' + version + ' (' + suffix + ')'; result.entryPackageVersion = version; result.entrySuffix = suffix; - - // /@rushstack/eslint-config/3.0.1_eslint@8.21.0+typescript@4.7.4 - // --> @rushstack/eslint-config 3.0.1 (eslint@8.21.0+typescript@4.7.4) - result.displayText = packageName + ' ' + version + ' (' + suffix + ')'; } else { - result.entryPackageVersion = versionPart; - // /@rushstack/eslint-config/3.0.1 // --> @rushstack/eslint-config 3.0.1 - result.displayText = packageName + ' ' + versionPart; + const version: string = rawEntryId.substring(lastSlashIndex + 1); + result.displayText = packageName + ' ' + version; + result.entryPackageVersion = version; } } + // Example: @babel+register@7.17.7_@babel+core@7.17.12 + const dotPnpmSubfolder: string = + result.entryPackageName.replace('/', '+') + + '@' + + result.entryPackageVersion + + (result.entrySuffix ? `_${result.entrySuffix}` : ''); + // Example: // common/temp/default/node_modules/.pnpm // /@babel+register@7.17.7_@babel+core@7.17.12 // /node_modules/@babel/register - result.packageJsonFolderPath = - `common/temp/${subspaceName}/node_modules/.pnpm/` + - result.entryPackageName.replace('/', '+') + - '@' + - result.entryPackageVersion + - (result.entrySuffix ? `_${result.entrySuffix}` : '') + - '/node_modules/' + - result.entryPackageName; + result.packageJsonFolderPath = lockfilePath.join( + pnpmLockfileFolder, + `node_modules/.pnpm/` + dotPnpmSubfolder + '/node_modules/' + result.entryPackageName + ); } const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); - parseDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData); + parseDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData, workspace); return lockfileEntry; } @@ -211,59 +218,68 @@ function createLockfileEntry(options: { * * @returns A list of all the LockfileEntries in the lockfile. */ -export function generateLockfileGraph( - workspace: IJsonLfxWorkspace, - lockfileJson: unknown, - subspaceName?: string -): LfxGraph { +export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfxWorkspace): LfxGraph { const lockfile: lockfileTypes.Lockfile = lockfileJson as lockfileTypes.Lockfile; - let pnpmLockfileVersion: PnpmLockfileVersion = PnpmLockfileVersion.V5; - if (parseInt(lockfile.lockfileVersion.toString(), 10) === 6) { - pnpmLockfileVersion = PnpmLockfileVersion.V6; - } - if (lockfile.packages && pnpmLockfileVersion === PnpmLockfileVersion.V6) { - const updatedPackages: lockfileTypes.PackageSnapshots = {}; - for (const [dependencyPath, dependency] of Object.entries(lockfile.packages)) { - updatedPackages[convertLockfileV6DepPathToV5DepPath(dependencyPath) as pnpmTypes.DepPath] = dependency; - } - lockfile.packages = updatedPackages; + let pnpmLockfileVersion: PnpmLockfileVersion; + switch (lockfile.lockfileVersion.toString()) { + case '5.4': + pnpmLockfileVersion = 54; + break; + case '6': + case '6.0': + pnpmLockfileVersion = 60; + break; + case '9': + case '9.0': + pnpmLockfileVersion = 90; + break; + default: + throw new Error('Unsupported PNPM lockfile version ' + JSON.stringify(lockfile.lockfileVersion)); } const lfxGraph: LfxGraph = new LfxGraph(workspace); const allEntries: LfxGraphEntry[] = lfxGraph.entries; - const allEntriesById: { [key: string]: LfxGraphEntry } = {}; + const allEntriesById: Map = new Map(); const allImporters: LfxGraphEntry[] = []; + + // "Importers" are the local workspace projects if (lockfile.importers) { - // Find duplicate importer names + // Normally the UX shows the concise project folder name. However in the case of duplicates + // (where two projects use the same folder name), then we will need to disambiguate. const baseNames: Set = new Set(); const duplicates: Set = new Set(); for (const importerKey of Object.keys(lockfile.importers)) { - const baseName: string | null = new Path(importerKey).basename(); - if (baseName) { - if (baseNames.has(baseName)) { - duplicates.add(baseName); - } - baseNames.add(baseName); + const baseName: string = lockfilePath.getBaseNameOf(importerKey); + if (baseNames.has(baseName)) { + duplicates.add(baseName); } + baseNames.add(baseName); } - for (const [importerKey, importerValue] of Object.entries(lockfile.importers)) { - // console.log('normalized importer key: ', new Path(importerKey).makeAbsolute('/').toString()); + const isRushWorkspace: boolean = workspace.rushConfig !== undefined; + + for (const importerKey of Object.keys(lockfile.importers)) { + if (isRushWorkspace && importerKey === '.') { + // Discard the synthetic package.json file created by Rush under common/temp + // continue; + } + + const importerValue: lockfileTypes.ProjectSnapshot = + lockfile.importers[importerKey as pnpmTypes.ProjectId]; - // const normalizedPath = new Path(importerKey).makeAbsolute('/').toString(); const importer: LfxGraphEntry = createLockfileEntry({ - // entryId: normalizedPath, - rawEntryId: importerKey, kind: LfxGraphEntryKind.Project, + rawEntryId: importerKey, rawYamlData: importerValue, duplicates, - subspaceName + workspace, + pnpmLockfileVersion }); allImporters.push(importer); allEntries.push(importer); - allEntriesById[importer.entryId] = importer; + allEntriesById.set(importer.entryId, importer); } } @@ -273,16 +289,16 @@ export function generateLockfileGraph( // const normalizedPath = new Path(dependencyKey).makeAbsolute('/').toString(); const currEntry: LfxGraphEntry = createLockfileEntry({ - // entryId: normalizedPath, - rawEntryId: dependencyKey, kind: LfxGraphEntryKind.Package, + rawEntryId: dependencyKey, rawYamlData: dependencyValue as lockfileTypes.PackageSnapshot, - subspaceName + workspace, + pnpmLockfileVersion }); allPackages.push(currEntry); allEntries.push(currEntry); - allEntriesById[dependencyKey] = currEntry; + allEntriesById.set(dependencyKey, currEntry); } } @@ -294,7 +310,7 @@ export function generateLockfileGraph( continue; } - const matchedEntry: LfxGraphEntry = allEntriesById[dependency.entryId]; + const matchedEntry: LfxGraphEntry | undefined = allEntriesById.get(dependency.entryId); if (matchedEntry) { // Create a two-way link between the dependency and the entry dependency.resolvedEntry = matchedEntry; diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap index 1be82a27f6d..3c19dff2d8a 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap @@ -10,13 +10,13 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` resolvedEntryJsonId: 5 version: 5.0.2 displayText: 'Project: duplicate (duplicate-1/duplicate)' - entryId: project:./common/temp/undefined/duplicate-1/duplicate + entryId: project:duplicate-1/duplicate entryPackageName: duplicate (duplicate-1/duplicate) entryPackageVersion: '' entrySuffix: '' jsonId: 0 kind: 1 - packageJsonFolderPath: ./common/temp/undefined/duplicate-1/duplicate + packageJsonFolderPath: duplicate-1/duplicate rawEntryId: duplicate-1/duplicate referrerJsonIds: [] transitivePeerDependencies: [] @@ -28,13 +28,13 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` resolvedEntryJsonId: 4 version: 2.1.2 displayText: 'Project: duplicate (duplicate-2/duplicate)' - entryId: project:./common/temp/undefined/duplicate-2/duplicate + entryId: project:duplicate-2/duplicate entryPackageName: duplicate (duplicate-2/duplicate) entryPackageVersion: '' entrySuffix: '' jsonId: 1 kind: 1 - packageJsonFolderPath: ./common/temp/undefined/duplicate-2/duplicate + packageJsonFolderPath: duplicate-2/duplicate rawEntryId: duplicate-2/duplicate referrerJsonIds: [] transitivePeerDependencies: [] @@ -52,7 +52,7 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` entrySuffix: '' jsonId: 2 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/color-convert@3.1.2/node_modules/color-convert + packageJsonFolderPath: node_modules/.pnpm/color-convert@3.1.2/node_modules/color-convert rawEntryId: /color-convert/3.1.2 referrerJsonIds: - 5 @@ -65,7 +65,7 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` entrySuffix: '' jsonId: 3 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/color-name@2.0.2/node_modules/color-name + packageJsonFolderPath: node_modules/.pnpm/color-name@2.0.2/node_modules/color-name rawEntryId: /color-name/2.0.2 referrerJsonIds: - 2 @@ -85,7 +85,7 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` entrySuffix: '' jsonId: 4 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/color-string@2.1.2/node_modules/color-string + packageJsonFolderPath: node_modules/.pnpm/color-string@2.1.2/node_modules/color-string rawEntryId: /color-string/2.1.2 referrerJsonIds: - 1 @@ -111,13 +111,14 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` entrySuffix: '' jsonId: 5 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/color@5.0.2/node_modules/color + packageJsonFolderPath: node_modules/.pnpm/color@5.0.2/node_modules/color rawEntryId: /color/5.0.2 referrerJsonIds: - 0 transitivePeerDependencies: [] workspace: - pnpmLockfilePath: common/temp/pnpm-lock.yaml - workspaceRootFolder: /repo + pnpmLockfileFolder: '' + pnpmLockfilePath: pnpm-lock.yaml + workspaceRootFullPath: /repo " `; diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap index f37d9721956..362b1874547 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap @@ -3,38 +3,38 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` "entries: - dependencies: [] - displayText: '' - entryId: '' - entryPackageName: '' + displayText: 'Project: .' + entryId: project:common/temp + entryPackageName: . entryPackageVersion: '' entrySuffix: '' jsonId: 0 kind: 1 - packageJsonFolderPath: '' + packageJsonFolderPath: common/temp rawEntryId: . referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} resolvedEntryJsonId: 4 version: link:../d displayText: 'Project: a' - entryId: project:./common/projects/a + entryId: project:projects/a entryPackageName: a entryPackageVersion: '' entrySuffix: '' jsonId: 1 kind: 1 - packageJsonFolderPath: ./common/projects/a + packageJsonFolderPath: projects/a rawEntryId: ../../projects/a referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} resolvedEntryJsonId: 4 @@ -46,19 +46,19 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` resolvedEntryJsonId: 12 version: 2.0.0 displayText: 'Project: b' - entryId: project:./common/projects/b + entryId: project:projects/b entryPackageName: b entryPackageVersion: '' entrySuffix: '' jsonId: 2 kind: 1 - packageJsonFolderPath: ./common/projects/b + packageJsonFolderPath: projects/b rawEntryId: ../../projects/b referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/e + entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} resolvedEntryJsonId: 5 @@ -76,19 +76,19 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` resolvedEntryJsonId: 11 version: 1.0.0 displayText: 'Project: c' - entryId: project:./common/projects/c + entryId: project:projects/c entryPackageName: c entryPackageVersion: '' entrySuffix: '' jsonId: 3 kind: 1 - packageJsonFolderPath: ./common/projects/c + packageJsonFolderPath: projects/c rawEntryId: ../../projects/c referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/e + entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} resolvedEntryJsonId: 5 @@ -106,13 +106,13 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` resolvedEntryJsonId: 12 version: 2.0.0 displayText: 'Project: d' - entryId: project:./common/projects/d + entryId: project:projects/d entryPackageName: d entryPackageVersion: '' entrySuffix: '' jsonId: 4 kind: 1 - packageJsonFolderPath: ./common/projects/d + packageJsonFolderPath: projects/d rawEntryId: ../../projects/d referrerJsonIds: - 1 @@ -126,13 +126,13 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` resolvedEntryJsonId: 13 version: 3.0.0 displayText: 'Project: e' - entryId: project:./common/projects/e + entryId: project:projects/e entryPackageName: e entryPackageVersion: '' entrySuffix: '' jsonId: 5 kind: 1 - packageJsonFolderPath: ./common/projects/e + packageJsonFolderPath: projects/e rawEntryId: ../../projects/e referrerJsonIds: - 3 @@ -158,7 +158,7 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entrySuffix: '@rushstack+n@2.0.0' jsonId: 6 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+j@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/j + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+j@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/j rawEntryId: /@rushstack/j/1.0.0_@rushstack+n@2.0.0 referrerJsonIds: - 4 @@ -178,7 +178,7 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entrySuffix: '@rushstack+m@1.0.0' jsonId: 7 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+k@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/k + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+k@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/k rawEntryId: /@rushstack/k/1.0.0_@rushstack+m@1.0.0 referrerJsonIds: - 3 @@ -199,8 +199,7 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entrySuffix: wxpgugna4ivthu7yyu4fmciltu jsonId: 8 kind: 2 - packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+k@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/k + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+k@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/k rawEntryId: /@rushstack/k/1.0.0_wxpgugna4ivthu7yyu4fmciltu referrerJsonIds: - 6 @@ -237,7 +236,7 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entrySuffix: '@rushstack+m@1.0.0' jsonId: 9 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+l@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/l + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+l@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/l rawEntryId: /@rushstack/l/1.0.0_@rushstack+m@1.0.0 referrerJsonIds: - 7 @@ -278,8 +277,7 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entrySuffix: wxpgugna4ivthu7yyu4fmciltu jsonId: 10 kind: 2 - packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+l@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/l + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+l@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/l rawEntryId: /@rushstack/l/1.0.0_wxpgugna4ivthu7yyu4fmciltu referrerJsonIds: - 8 @@ -292,7 +290,7 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entrySuffix: '' jsonId: 11 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m rawEntryId: /@rushstack/m/1.0.0 referrerJsonIds: - 3 @@ -308,7 +306,7 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entrySuffix: '' jsonId: 12 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n rawEntryId: /@rushstack/n/2.0.0 referrerJsonIds: - 2 @@ -323,16 +321,17 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entrySuffix: '' jsonId: 13 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n rawEntryId: /@rushstack/n/3.0.0 referrerJsonIds: - 5 transitivePeerDependencies: [] workspace: + pnpmLockfileFolder: common/temp pnpmLockfilePath: common/temp/pnpm-lock.yaml rushConfig: rushVersion: 5.83.3 subspaceName: '' - workspaceRootFolder: /repo + workspaceRootFullPath: /repo " `; diff --git a/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts b/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts index 138475b40b0..74d99eb35de 100644 --- a/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts +++ b/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts @@ -24,7 +24,7 @@ export async function loadAndSerializeLFxGraphAsync(options: { { convertLineEndings: NewlineKind.Lf } ); const lockfileObject = yaml.load(lockfileYaml); - const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(options.workspace, lockfileObject); + const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(lockfileObject, options.workspace); const serializedObject: IJsonLfxGraph = lfxGraphSerializer.serializeToJson(graph); const serializedYaml: string = yaml.dump(serializedObject, { noRefs: true, diff --git a/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts index fe9bae9ce9d..1a362d47c73 100644 --- a/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts @@ -6,8 +6,9 @@ import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; import * as graphTestHelpers from './graphTestHelpers'; export const workspace: IJsonLfxWorkspace = { - workspaceRootFolder: '/repo', + workspaceRootFullPath: '/repo', pnpmLockfilePath: 'pnpm-lock.yaml', + pnpmLockfileFolder: '', rushConfig: undefined }; diff --git a/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v5.4.test.ts b/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v5.4.test.ts index 3ca5deda4b8..af8dc57b59d 100644 --- a/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v5.4.test.ts +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v5.4.test.ts @@ -6,8 +6,9 @@ import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; import * as graphTestHelpers from './graphTestHelpers'; export const workspace: IJsonLfxWorkspace = { - workspaceRootFolder: '/repo', + workspaceRootFullPath: '/repo', pnpmLockfilePath: 'common/temp/pnpm-lock.yaml', + pnpmLockfileFolder: 'common/temp', rushConfig: { rushVersion: '5.83.3', subspaceName: '' diff --git a/apps/lockfile-explorer/src/graph/test/lockfile.test.ts b/apps/lockfile-explorer/src/graph/test/lockfile.test.ts index 8b89f8de00d..7e1a92a090c 100644 --- a/apps/lockfile-explorer/src/graph/test/lockfile.test.ts +++ b/apps/lockfile-explorer/src/graph/test/lockfile.test.ts @@ -8,7 +8,7 @@ import * as lfxGraphLoader from '../lfxGraphLoader'; describe('LockfileGeneration', () => { it('creates a valid bi-directional graph', () => { - const resolvedPackages = lfxGraphLoader.generateLockfileGraph(TEST_WORKSPACE, TEST_LOCKFILE).entries; + const resolvedPackages = lfxGraphLoader.generateLockfileGraph(TEST_LOCKFILE, TEST_WORKSPACE).entries; // Mapping of all the lockfile entries created by the lockfile const resolvedPackagesMap: { [key: string]: LfxGraphEntry } = {}; @@ -20,7 +20,7 @@ describe('LockfileGeneration', () => { // Ensure validity of the example lockfile entry expect(exampleLockfileImporter.rawEntryId).toBe('../../../apps/testApp1'); - expect(exampleLockfileImporter.entryId).toBe('project:./apps/testApp1'); + expect(exampleLockfileImporter.entryId).toBe('project:apps/testApp1'); // Test that dependencies are linked in the importer project expect(exampleLockfileImporter.dependencies.length).toBe(2); diff --git a/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts b/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts index 977cae423be..493ee2ca5e4 100644 --- a/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts +++ b/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts @@ -8,21 +8,21 @@ import { TEST_WORKSPACE, TEST_LOCKFILE } from './testLockfile'; describe('serializeToJson', () => { it('serializes a simple graph', () => { - const graph = lfxGraphLoader.generateLockfileGraph(TEST_WORKSPACE, TEST_LOCKFILE); + const graph = lfxGraphLoader.generateLockfileGraph(TEST_LOCKFILE, TEST_WORKSPACE); expect(lfxGraphSerializer.serializeToJson(graph)).toMatchInlineSnapshot(` Object { "entries": Array [ Object { "dependencies": Array [], - "displayText": "", - "entryId": "", - "entryPackageName": "", + "displayText": "Project: .", + "entryId": "project:common/temp/my-subspace", + "entryPackageName": ".", "entryPackageVersion": "", "entrySuffix": "", "jsonId": 0, "kind": 1, - "packageJsonFolderPath": "", + "packageJsonFolderPath": "common/temp/my-subspace", "rawEntryId": ".", "referrerJsonIds": Array [], "transitivePeerDependencies": Array [], @@ -55,13 +55,13 @@ Object { }, ], "displayText": "Project: testApp1", - "entryId": "project:./apps/testApp1", + "entryId": "project:apps/testApp1", "entryPackageName": "testApp1", "entryPackageVersion": "", "entrySuffix": "", "jsonId": 1, "kind": 1, - "packageJsonFolderPath": "./apps/testApp1", + "packageJsonFolderPath": "apps/testApp1", "rawEntryId": "../../../apps/testApp1", "referrerJsonIds": Array [], "transitivePeerDependencies": Array [], @@ -75,7 +75,7 @@ Object { "entrySuffix": "", "jsonId": 2, "kind": 2, - "packageJsonFolderPath": "common/temp/undefined/node_modules/.pnpm/@testPackage+core@1.7.1/node_modules/@testPackage/core", + "packageJsonFolderPath": "common/temp/my-subspace/node_modules/.pnpm/@testPackage+core@1.7.1/node_modules/@testPackage/core", "rawEntryId": "/@testPackage/core/1.7.1", "referrerJsonIds": Array [ 1, @@ -91,7 +91,7 @@ Object { "entrySuffix": "", "jsonId": 3, "kind": 2, - "packageJsonFolderPath": "common/temp/undefined/node_modules/.pnpm/@testPackage2+core@1.7.1/node_modules/@testPackage2/core", + "packageJsonFolderPath": "common/temp/my-subspace/node_modules/.pnpm/@testPackage2+core@1.7.1/node_modules/@testPackage2/core", "rawEntryId": "/@testPackage2/core/1.7.1", "referrerJsonIds": Array [ 1, @@ -100,16 +100,20 @@ Object { }, ], "workspace": Object { - "pnpmLockfilePath": "/test/pnpm-lock.yaml", - "rushConfig": undefined, - "workspaceRootFolder": "/test", + "pnpmLockfileFolder": "common/temp/my-subspace", + "pnpmLockfilePath": "common/temp/my-subspace/pnpm-lock.yaml", + "rushConfig": Object { + "rushVersion": "0.0.0", + "subspaceName": "my-subspace", + }, + "workspaceRootFullPath": "/repo", }, } `); }); it('deserializes a simple graph', () => { - const originalGraph = lfxGraphLoader.generateLockfileGraph(TEST_WORKSPACE, TEST_LOCKFILE); + const originalGraph = lfxGraphLoader.generateLockfileGraph(TEST_LOCKFILE, TEST_WORKSPACE); const serialized: string = JSON.stringify( lfxGraphSerializer.serializeToJson(originalGraph), diff --git a/apps/lockfile-explorer/src/graph/test/testLockfile.ts b/apps/lockfile-explorer/src/graph/test/testLockfile.ts index 0ba13018fbc..88f898cbdb2 100644 --- a/apps/lockfile-explorer/src/graph/test/testLockfile.ts +++ b/apps/lockfile-explorer/src/graph/test/testLockfile.ts @@ -4,13 +4,17 @@ import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; export const TEST_WORKSPACE: IJsonLfxWorkspace = { - workspaceRootFolder: '/test', - pnpmLockfilePath: '/test/pnpm-lock.yaml', - rushConfig: undefined + workspaceRootFullPath: '/repo', + pnpmLockfilePath: 'common/temp/my-subspace/pnpm-lock.yaml', + pnpmLockfileFolder: 'common/temp/my-subspace', + rushConfig: { + rushVersion: '0.0.0', + subspaceName: 'my-subspace' + } }; export const TEST_LOCKFILE = { - lockfileVersion: 5.3, + lockfileVersion: 5.4, importers: { '.': { specifiers: {} diff --git a/apps/lockfile-explorer/src/utils/init.ts b/apps/lockfile-explorer/src/utils/init.ts index 73856ea7943..dd30057b477 100644 --- a/apps/lockfile-explorer/src/utils/init.ts +++ b/apps/lockfile-explorer/src/utils/init.ts @@ -33,18 +33,20 @@ export const init = (options: { const subspace: Subspace = rushConfiguration.getSubspace(subspaceName); const workspaceFolder: string = subspace.getSubspaceTempFolderPath(); - const pnpmLockfileLocation: string = path.resolve(workspaceFolder, 'pnpm-lock.yaml'); + const pnpmLockfileAbsolutePath: string = path.resolve(workspaceFolder, 'pnpm-lock.yaml'); + const pnpmLockfileRelativePath: string = path.relative(currentFolder, pnpmLockfileAbsolutePath); appState = { currentWorkingDirectory, appVersion, debugMode, lockfileExplorerProjectRoot, - pnpmLockfileLocation, + pnpmLockfileLocation: pnpmLockfileAbsolutePath, pnpmfileLocation: workspaceFolder + '/.pnpmfile.cjs', projectRoot: currentFolder, lfxWorkspace: { - workspaceRootFolder: currentFolder, - pnpmLockfilePath: Path.convertToSlashes(path.relative(currentFolder, pnpmLockfileLocation)), + workspaceRootFullPath: currentFolder, + pnpmLockfilePath: Path.convertToSlashes(pnpmLockfileRelativePath), + pnpmLockfileFolder: Path.convertToSlashes(path.basename(pnpmLockfileRelativePath)), rushConfig: { rushVersion: rushConfiguration.rushConfigurationJson.rushVersion, subspaceName: subspaceName ?? '' @@ -62,8 +64,9 @@ export const init = (options: { pnpmfileLocation: currentFolder + '/.pnpmfile.cjs', projectRoot: currentFolder, lfxWorkspace: { - workspaceRootFolder: currentFolder, + workspaceRootFullPath: currentFolder, pnpmLockfilePath: Path.convertToSlashes(path.relative(currentFolder, pnpmLockPath)), + pnpmLockfileFolder: '', rushConfig: undefined } }; From 4d37984cad5f073cca28dfeddd6a89fdfe63e8aa Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 01:55:50 -0700 Subject: [PATCH 05/17] Remove "@lifaon/path" dependency --- apps/lockfile-explorer/package.json | 1 - common/config/subspaces/default/pnpm-lock.yaml | 7 ------- common/config/subspaces/default/repo-state.json | 2 +- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/lockfile-explorer/package.json b/apps/lockfile-explorer/package.json index 2b5eae2114f..8c87281b4ab 100644 --- a/apps/lockfile-explorer/package.json +++ b/apps/lockfile-explorer/package.json @@ -61,7 +61,6 @@ }, "dependencies": { "tslib": "~2.8.1", - "@lifaon/path": "~2.1.0", "@microsoft/rush-lib": "workspace:*", "@pnpm/dependency-path-lockfile-pre-v9": "npm:@pnpm/dependency-path@~2.1.2", "@rushstack/node-core-library": "workspace:*", diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index fd8cdf97b46..dea2c108456 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -193,9 +193,6 @@ importers: ../../../apps/lockfile-explorer: dependencies: - '@lifaon/path': - specifier: ~2.1.0 - version: 2.1.0 '@microsoft/rush-lib': specifier: workspace:* version: link:../../libraries/rush-lib @@ -10174,10 +10171,6 @@ packages: resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} dev: false - /@lifaon/path@2.1.0: - resolution: {integrity: sha512-E+eJpDdwenIQCaYMMuCnteR34qAvXtHhHKjZOPB+hK4+R1yGcmWLLAEl2aklxCHx6w5VCKc8imx9AT05FGHhBw==} - dev: false - /@mdx-js/loader@1.6.22(react@17.0.2): resolution: {integrity: sha512-9CjGwy595NaxAYp0hF9B/A0lH6C8Rms97e2JS9d3jVUtILn6pT5i5IV965ra3lIWc7Rs1GG1tBdVF7dCowYe6Q==} dependencies: diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 655041c65b6..1fb2747cfad 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "38797dd39684687f2ae92bb0661933fe861efc98", + "pnpmShrinkwrapHash": "3b066865f3d1f4a47d2b84fba4a90a6f6da91e7e", "preferredVersionsHash": "61cd419c533464b580f653eb5f5a7e27fe7055ca" } From 320e5c8f93f4601fab06474475f9023bd4f073ad Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 01:59:31 -0700 Subject: [PATCH 06/17] Splite createLockfileEntry() into createProjectLockfileEntry() and createPackageLockfileEntry() --- .../src/graph/lfxGraphLoader.ts | 174 +++++++++++------- 1 file changed, 103 insertions(+), 71 deletions(-) diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index 7db12299704..493e0d81a84 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -18,7 +18,7 @@ import * as lockfilePath from './lockfilePath'; type PnpmLockfileVersion = 54 | 60 | 90; -function createLockfileDependency( +function createPackageLockfileDependency( name: string, version: string, dependencyType: LfxDependencyKind, @@ -76,7 +76,7 @@ function parseDependencies( if (node.dependencies) { for (const [pkgName, pkgVersion] of Object.entries(node.dependencies)) { dependencies.push( - createLockfileDependency( + createPackageLockfileDependency( pkgName, pkgVersion, LfxDependencyKind.Regular, @@ -90,7 +90,7 @@ function parseDependencies( if (node.devDependencies) { for (const [pkgName, pkgVersion] of Object.entries(node.devDependencies)) { dependencies.push( - createLockfileDependency( + createPackageLockfileDependency( pkgName, pkgVersion, LfxDependencyKind.Dev, @@ -104,7 +104,14 @@ function parseDependencies( if (node.peerDependencies) { for (const [pkgName, pkgVersion] of Object.entries(node.peerDependencies)) { dependencies.push( - createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Peer, lockfileEntry, node, workspace) + createPackageLockfileDependency( + pkgName, + pkgVersion, + LfxDependencyKind.Peer, + lockfileEntry, + node, + workspace + ) ); } } @@ -115,18 +122,17 @@ function parseDependencies( } } -function createLockfileEntry(options: { +function createProjectLockfileEntry(options: { rawEntryId: string; - kind: LfxGraphEntryKind; - rawYamlData: lockfileTypes.PackageSnapshot | lockfileTypes.ProjectSnapshot; + rawYamlData: lockfileTypes.ProjectSnapshot; duplicates?: Set; workspace: IJsonLfxWorkspace; pnpmLockfileVersion: PnpmLockfileVersion; }): LfxGraphEntry { - const { rawEntryId, kind, rawYamlData, duplicates, pnpmLockfileVersion, workspace } = options; + const { rawEntryId, rawYamlData, duplicates, workspace } = options; const result: ILfxGraphEntryOptions = { - kind, + kind: LfxGraphEntryKind.Project, entryId: '', rawEntryId: '', packageJsonFolderPath: '', @@ -142,71 +148,99 @@ function createLockfileEntry(options: { // Example: pnpmLockfileFolder = 'common/temp/my-subspace' const pnpmLockfileFolder: string = workspace.pnpmLockfileFolder; - if (kind === LfxGraphEntryKind.Project) { - // Example: rawEntryId = '../../../projects/a' - // Example: packageJsonFolderPath = 'projects/a' - result.packageJsonFolderPath = lockfilePath.getAbsolute(pnpmLockfileFolder, rawEntryId); - result.entryId = 'project:' + result.packageJsonFolderPath; + // Example: rawEntryId = '../../../projects/a' + // Example: packageJsonFolderPath = 'projects/a' + result.packageJsonFolderPath = lockfilePath.getAbsolute(pnpmLockfileFolder, rawEntryId); + result.entryId = 'project:' + result.packageJsonFolderPath; - const projectFolderName: string = lockfilePath.getBaseNameOf(rawEntryId); + const projectFolderName: string = lockfilePath.getBaseNameOf(rawEntryId); - if (!duplicates?.has(projectFolderName)) { - // TODO: The actual package.json name might not match its directory name, - // but we have to load package.json to determine it. - result.entryPackageName = projectFolderName; - } else { - result.entryPackageName = `${projectFolderName} (${result.packageJsonFolderPath})`; - } - result.displayText = `Project: ${result.entryPackageName}`; + if (!duplicates?.has(projectFolderName)) { + // TODO: The actual package.json name might not match its directory name, + // but we have to load package.json to determine it. + result.entryPackageName = projectFolderName; } else { - result.displayText = rawEntryId; + result.entryPackageName = `${projectFolderName} (${result.packageJsonFolderPath})`; + } + result.displayText = `Project: ${result.entryPackageName}`; - if (pnpmLockfileVersion === 54) { - if (!rawEntryId.startsWith('/')) { - throw new Error('Expecting leading "/" in path: ' + JSON.stringify(rawEntryId)); - } - const lastSlashIndex: number = rawEntryId.lastIndexOf('/'); - if (lastSlashIndex < 0) { - throw new Error('Expecting "/" in path: ' + JSON.stringify(rawEntryId)); - } - const packageName: string = rawEntryId.substring(1, lastSlashIndex); - result.entryPackageName = packageName; - - // /@rushstack/eslint-config/3.0.1_eslint@8.21.0+typescript@4.7.4 - // --> @rushstack/eslint-config 3.0.1 (eslint@8.21.0+typescript@4.7.4) - const underscoreIndex: number = rawEntryId.indexOf('_', lastSlashIndex); - if (underscoreIndex > 0) { - const version: string = rawEntryId.substring(lastSlashIndex + 1, underscoreIndex); - const suffix: string = rawEntryId.substring(underscoreIndex + 1); - result.displayText = packageName + ' ' + version + ' (' + suffix + ')'; - result.entryPackageVersion = version; - result.entrySuffix = suffix; - } else { - // /@rushstack/eslint-config/3.0.1 - // --> @rushstack/eslint-config 3.0.1 - const version: string = rawEntryId.substring(lastSlashIndex + 1); - result.displayText = packageName + ' ' + version; - result.entryPackageVersion = version; - } - } + const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); + parseDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData, workspace); + return lockfileEntry; +} - // Example: @babel+register@7.17.7_@babel+core@7.17.12 - const dotPnpmSubfolder: string = - result.entryPackageName.replace('/', '+') + - '@' + - result.entryPackageVersion + - (result.entrySuffix ? `_${result.entrySuffix}` : ''); - - // Example: - // common/temp/default/node_modules/.pnpm - // /@babel+register@7.17.7_@babel+core@7.17.12 - // /node_modules/@babel/register - result.packageJsonFolderPath = lockfilePath.join( - pnpmLockfileFolder, - `node_modules/.pnpm/` + dotPnpmSubfolder + '/node_modules/' + result.entryPackageName - ); +function createPackageLockfileEntry(options: { + rawEntryId: string; + rawYamlData: lockfileTypes.PackageSnapshot; + workspace: IJsonLfxWorkspace; + pnpmLockfileVersion: PnpmLockfileVersion; +}): LfxGraphEntry { + const { rawEntryId, rawYamlData, pnpmLockfileVersion, workspace } = options; + + const result: ILfxGraphEntryOptions = { + kind: LfxGraphEntryKind.Package, + entryId: '', + rawEntryId: '', + packageJsonFolderPath: '', + entryPackageName: '', + displayText: '', + entryPackageVersion: '', + entrySuffix: '' + }; + + result.rawEntryId = rawEntryId; + + // Example: pnpmLockfilePath = 'common/temp/my-subspace/pnpm-lock.yaml' + // Example: pnpmLockfileFolder = 'common/temp/my-subspace' + const pnpmLockfileFolder: string = workspace.pnpmLockfileFolder; + + result.displayText = rawEntryId; + + if (pnpmLockfileVersion === 54) { + if (!rawEntryId.startsWith('/')) { + throw new Error('Expecting leading "/" in path: ' + JSON.stringify(rawEntryId)); + } + const lastSlashIndex: number = rawEntryId.lastIndexOf('/'); + if (lastSlashIndex < 0) { + throw new Error('Expecting "/" in path: ' + JSON.stringify(rawEntryId)); + } + const packageName: string = rawEntryId.substring(1, lastSlashIndex); + result.entryPackageName = packageName; + + // /@rushstack/eslint-config/3.0.1_eslint@8.21.0+typescript@4.7.4 + // --> @rushstack/eslint-config 3.0.1 (eslint@8.21.0+typescript@4.7.4) + const underscoreIndex: number = rawEntryId.indexOf('_', lastSlashIndex); + if (underscoreIndex > 0) { + const version: string = rawEntryId.substring(lastSlashIndex + 1, underscoreIndex); + const suffix: string = rawEntryId.substring(underscoreIndex + 1); + result.displayText = packageName + ' ' + version + ' (' + suffix + ')'; + result.entryPackageVersion = version; + result.entrySuffix = suffix; + } else { + // /@rushstack/eslint-config/3.0.1 + // --> @rushstack/eslint-config 3.0.1 + const version: string = rawEntryId.substring(lastSlashIndex + 1); + result.displayText = packageName + ' ' + version; + result.entryPackageVersion = version; + } } + // Example: @babel+register@7.17.7_@babel+core@7.17.12 + const dotPnpmSubfolder: string = + result.entryPackageName.replace('/', '+') + + '@' + + result.entryPackageVersion + + (result.entrySuffix ? `_${result.entrySuffix}` : ''); + + // Example: + // common/temp/default/node_modules/.pnpm + // /@babel+register@7.17.7_@babel+core@7.17.12 + // /node_modules/@babel/register + result.packageJsonFolderPath = lockfilePath.join( + pnpmLockfileFolder, + `node_modules/.pnpm/` + dotPnpmSubfolder + '/node_modules/' + result.entryPackageName + ); + const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); parseDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData, workspace); return lockfileEntry; @@ -269,8 +303,7 @@ export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfx const importerValue: lockfileTypes.ProjectSnapshot = lockfile.importers[importerKey as pnpmTypes.ProjectId]; - const importer: LfxGraphEntry = createLockfileEntry({ - kind: LfxGraphEntryKind.Project, + const importer: LfxGraphEntry = createProjectLockfileEntry({ rawEntryId: importerKey, rawYamlData: importerValue, duplicates, @@ -288,8 +321,7 @@ export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfx for (const [dependencyKey, dependencyValue] of Object.entries(lockfile.packages ?? {})) { // const normalizedPath = new Path(dependencyKey).makeAbsolute('/').toString(); - const currEntry: LfxGraphEntry = createLockfileEntry({ - kind: LfxGraphEntryKind.Package, + const currEntry: LfxGraphEntry = createPackageLockfileEntry({ rawEntryId: dependencyKey, rawYamlData: dependencyValue as lockfileTypes.PackageSnapshot, workspace, From fedfaab8a2d0464b43414a10a2a47809bae945df Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 02:25:51 -0700 Subject: [PATCH 07/17] Upgrade to use @pnpm/lockfile.types@1002.0.1 --- apps/lockfile-explorer/package.json | 2 +- .../src/cli/lint/actions/CheckAction.ts | 8 ++-- .../src/graph/lfxGraphLoader.ts | 4 +- .../lfxGraph-website-sample-1-v6.0.test.ts | 3 +- .../subspaces/default/common-versions.json | 3 ++ .../config/subspaces/default/pnpm-lock.yaml | 39 ++++++++++++------- .../config/subspaces/default/repo-state.json | 2 +- 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/apps/lockfile-explorer/package.json b/apps/lockfile-explorer/package.json index 8c87281b4ab..62e03b0be7f 100644 --- a/apps/lockfile-explorer/package.json +++ b/apps/lockfile-explorer/package.json @@ -55,7 +55,7 @@ "@types/update-notifier": "~6.0.1", "eslint": "~9.25.1", "local-node-rig": "workspace:*", - "@pnpm/lockfile-types": "7.1.3", + "@pnpm/lockfile.types": "1002.0.1", "@pnpm/types": "1000.8.0", "@types/semver": "7.5.0" }, diff --git a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts index 6709a462796..12390a1b274 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts @@ -7,7 +7,7 @@ import { RushConfiguration, type RushConfigurationProject, type Subspace } from import path from 'path'; import yaml from 'js-yaml'; import semver from 'semver'; -import type * as lockfileTypes from '@pnpm/lockfile-types'; +import type * as lockfileTypes from '@pnpm/lockfile.types'; import type * as pnpmTypes from '@pnpm/types'; import { AlreadyReportedError, Async, FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library'; @@ -41,7 +41,7 @@ export class CheckAction extends CommandLineAction { private _rushConfiguration!: RushConfiguration; private _checkedProjects: Set; - private _docMap: Map; + private _docMap: Map; public constructor(parser: LintCommandLineParser) { super({ @@ -104,12 +104,12 @@ export class CheckAction extends CommandLineAction { const projectFolder: string = project.projectFolder; const subspace: Subspace = project.subspace; const shrinkwrapFilename: string = subspace.getCommittedShrinkwrapFilePath(); - let doc: lockfileTypes.Lockfile; + let doc: lockfileTypes.LockfileObject; if (this._docMap.has(shrinkwrapFilename)) { doc = this._docMap.get(shrinkwrapFilename)!; } else { const pnpmLockfileText: string = await FileSystem.readFileAsync(shrinkwrapFilename); - doc = yaml.load(pnpmLockfileText) as lockfileTypes.Lockfile; + doc = yaml.load(pnpmLockfileText) as lockfileTypes.LockfileObject; this._docMap.set(shrinkwrapFilename, doc); } const { importers, lockfileVersion, packages } = doc; diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index 493e0d81a84..15669791ce4 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type * as lockfileTypes from '@pnpm/lockfile-types'; +import type * as lockfileTypes from '@pnpm/lockfile.types'; import type * as pnpmTypes from '@pnpm/types'; import { @@ -253,7 +253,7 @@ function createPackageLockfileEntry(options: { * @returns A list of all the LockfileEntries in the lockfile. */ export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfxWorkspace): LfxGraph { - const lockfile: lockfileTypes.Lockfile = lockfileJson as lockfileTypes.Lockfile; + const lockfile: lockfileTypes.LockfileObject = lockfileJson as lockfileTypes.LockfileObject; let pnpmLockfileVersion: PnpmLockfileVersion; switch (lockfile.lockfileVersion.toString()) { diff --git a/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v6.0.test.ts b/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v6.0.test.ts index dd381d93a96..90231d947c4 100644 --- a/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v6.0.test.ts +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v6.0.test.ts @@ -6,8 +6,9 @@ import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; import * as graphTestHelpers from './graphTestHelpers'; export const workspace: IJsonLfxWorkspace = { - workspaceRootFolder: '/repo', + workspaceRootFullPath: '/repo', pnpmLockfilePath: 'common/temp/pnpm-lock.yaml', + pnpmLockfileFolder: 'common/temp', rushConfig: { rushVersion: '5.158.1', subspaceName: '' diff --git a/common/config/subspaces/default/common-versions.json b/common/config/subspaces/default/common-versions.json index bdf76be499b..080e192c513 100644 --- a/common/config/subspaces/default/common-versions.json +++ b/common/config/subspaces/default/common-versions.json @@ -78,6 +78,9 @@ * This design avoids unnecessary churn in this file. */ "allowedAlternativeVersions": { + // Allow Lockfile Explorer to support PNPM 9.x + // TODO: Remove this after Rush adds support for PNPM 9.x + "@pnpm/lockfile.types": ["1002.0.1"], "@typescript-eslint/parser": [ "~6.19.0" // Used by build-tests/eslint-7(-*)-test / build-tests/eslint-bulk-suppressions-test-legacy ], diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index dea2c108456..a42c8217d0c 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -233,9 +233,9 @@ importers: specifier: ~5.1.0 version: 5.1.0 devDependencies: - '@pnpm/lockfile-types': - specifier: 7.1.3 - version: 7.1.3 + '@pnpm/lockfile.types': + specifier: 1002.0.1 + version: 1002.0.1 '@pnpm/types': specifier: 1000.8.0 version: 1000.8.0 @@ -10487,13 +10487,6 @@ packages: ramda: 0.27.2 dev: false - /@pnpm/lockfile-types@7.1.3: - resolution: {integrity: sha512-ifMGKjiBFweZiI9nRYw+N4oP42tDaxHGzla/fuEqn8rOLzITJKfD9G2KfF7u8ZfZyW9lHFL/FsN9t1+q6L+qyQ==} - engines: {node: '>=18.12'} - dependencies: - '@pnpm/types': 11.1.0 - dev: true - /@pnpm/lockfile.types@1.0.3: resolution: {integrity: sha512-A7vUWktnhDkrIs+WmXm7AdffJVyVYJpQUEouya/DYhB+Y+tQ3BXjZ6CV0KybqLgI/8AZErgCJqFxA0GJH6QDjA==} engines: {node: '>=18.12'} @@ -10501,6 +10494,15 @@ packages: '@pnpm/patching.types': 1.0.0 '@pnpm/types': 12.2.0 + /@pnpm/lockfile.types@1002.0.1: + resolution: {integrity: sha512-anzBtzb78rf2KRExS8R38v4nyiU7b9ZMUsyzRdWpo+rfCmLUupjIxvasVlDgsf5pV7tbcBPASOamQ2G5V8IGAQ==} + engines: {node: '>=18.12'} + dependencies: + '@pnpm/patching.types': 1000.1.0 + '@pnpm/resolver-base': 1005.0.1 + '@pnpm/types': 1000.8.0 + dev: true + /@pnpm/logger@4.0.0: resolution: {integrity: sha512-SIShw+k556e7S7tLZFVSIHjCdiVog1qWzcKW2RbLEHPItdisAFVNIe34kYd9fMSswTlSRLS/qRjw3ZblzWmJ9Q==} engines: {node: '>=12.17'} @@ -10522,6 +10524,11 @@ packages: resolution: {integrity: sha512-juCdQCC1USqLcOhVPl1tYReoTO9YH4fTullMnFXXcmpsDM7Dkn3tzuOQKC3oPoJ2ozv+0EeWWMtMGqn2+IM3pQ==} engines: {node: '>=18.12'} + /@pnpm/patching.types@1000.1.0: + resolution: {integrity: sha512-Zib2ysLctRnWM4KXXlljR44qSKwyEqYmLk+8VPBDBEK3l5Gp5mT3N4ix9E4qjYynvFqahumsxzOfxOYQhUGMGw==} + engines: {node: '>=18.12'} + dev: true + /@pnpm/read-modules-dir@2.0.3: resolution: {integrity: sha512-i9OgRvSlxrTS9a2oXokhDxvQzDtfqtsooJ9jaGoHkznue5aFCTSrNZFQ6M18o8hC03QWfnxaKi0BtOvNkKu2+A==} engines: {node: '>=10.13'} @@ -10557,6 +10564,13 @@ packages: strip-bom: 4.0.0 dev: false + /@pnpm/resolver-base@1005.0.1: + resolution: {integrity: sha512-NBha12KjFMKwaG1BWTCtgr/RprNQhXItCBkzc8jZuVU0itAHRQhEykexna9K8XjAtYxZ9rhvir0T5a7fTB23yQ==} + engines: {node: '>=18.12'} + dependencies: + '@pnpm/types': 1000.8.0 + dev: true + /@pnpm/types@1000.6.0: resolution: {integrity: sha512-6PsMNe98VKPGcg6LnXSW/LE3YfJ77nj+bPKiRjYRWAQLZ+xXjEQRaR0dAuyjCmchlv4wR/hpnMVRS21/fCod5w==} engines: {node: '>=18.12'} @@ -10567,11 +10581,6 @@ packages: engines: {node: '>=18.12'} dev: true - /@pnpm/types@11.1.0: - resolution: {integrity: sha512-wnlOhu7hjv9/qsf2cbK0YqpaV9c4LS69Utxd+r8hq/GWhyrOHcM1QOlfQb0Mzci0q4DDgB8VXT4dhBnEBL4c5g==} - engines: {node: '>=18.12'} - dev: true - /@pnpm/types@12.2.0: resolution: {integrity: sha512-5RtwWhX39j89/Tmyv2QSlpiNjErA357T/8r1Dkg+2lD3P7RuS7Xi2tChvmOC3VlezEFNcWnEGCOeKoGRkDuqFA==} engines: {node: '>=18.12'} diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 1fb2747cfad..e231ac62310 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "3b066865f3d1f4a47d2b84fba4a90a6f6da91e7e", + "pnpmShrinkwrapHash": "8b58fd385a454cbee88a19ce0444a259ca7f28a6", "preferredVersionsHash": "61cd419c533464b580f653eb5f5a7e27fe7055ca" } From 90b993e6025a6c32a49e347ee411a2d52760ed1e Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 03:42:05 -0700 Subject: [PATCH 08/17] Implement support for lockfile version 6.0 --- apps/lockfile-explorer/.vscode/launch.json | 2 +- .../src/graph/lfxGraphLoader.ts | 250 ++++++++++++------ ...fxGraph-website-sample-1-v6.0.test.ts.snap | 110 ++++---- 3 files changed, 231 insertions(+), 131 deletions(-) diff --git a/apps/lockfile-explorer/.vscode/launch.json b/apps/lockfile-explorer/.vscode/launch.json index b1a4e7ccc89..2ee133e0e98 100644 --- a/apps/lockfile-explorer/.vscode/launch.json +++ b/apps/lockfile-explorer/.vscode/launch.json @@ -20,7 +20,7 @@ "name": "Single Jest test", "program": "${workspaceFolder}/node_modules/@rushstack/heft/lib/start.js", "cwd": "${workspaceFolder}", - "args": ["--debug", "test", "--clean", "-u", "--test-path-pattern", "edge-cases"], + "args": ["--debug", "test", "--clean", "-u", "--test-path-pattern", "lfxGraph-website-sample-1-v6.0.test"], "console": "integratedTerminal", "sourceMaps": true }, diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index 15669791ce4..ab7cb7d9540 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -3,6 +3,7 @@ import type * as lockfileTypes from '@pnpm/lockfile.types'; import type * as pnpmTypes from '@pnpm/types'; +import { Text } from '@rushstack/node-core-library'; import { type ILfxGraphDependencyOptions, @@ -17,15 +18,25 @@ import { import * as lockfilePath from './lockfilePath'; type PnpmLockfileVersion = 54 | 60 | 90; +type PeerDependenciesMeta = lockfileTypes.LockfilePackageInfo['peerDependenciesMeta']; + +function createPackageLockfileDependency(options: { + name: string; + version: string; + kind: LfxDependencyKind; + containingEntry: LfxGraphEntry; + peerDependenciesMeta?: PeerDependenciesMeta; + pnpmLockfileVersion: PnpmLockfileVersion; +}): LfxGraphDependency { + const { + name, + version, + kind: dependencyType, + containingEntry, + peerDependenciesMeta, + pnpmLockfileVersion + } = options; -function createPackageLockfileDependency( - name: string, - version: string, - dependencyType: LfxDependencyKind, - containingEntry: LfxGraphEntry, - node: Partial | undefined, - workspace: IJsonLfxWorkspace -): LfxGraphDependency { const result: ILfxGraphDependencyOptions = { name, version, @@ -45,73 +56,73 @@ function createPackageLockfileDependency( } else if (result.version.startsWith('/')) { result.entryId = version; } else if (result.dependencyType === LfxDependencyKind.Peer) { - if (node?.peerDependencies) { - result.peerDependencyMeta = { - name: result.name, - version: node.peerDependencies[result.name], - optional: - node.peerDependenciesMeta && node.peerDependenciesMeta[result.name] - ? node.peerDependenciesMeta[result.name].optional - : false - }; - result.entryId = 'Peer: ' + result.name; - } else { - console.error('Peer dependencies info missing!', node); - } + result.peerDependencyMeta = { + name: result.name, + version: version, + optional: + peerDependenciesMeta && peerDependenciesMeta[result.name] + ? peerDependenciesMeta[result.name].optional + : false + }; + result.entryId = 'Peer: ' + result.name; } else { - result.entryId = '/' + result.name + '/' + result.version; + // Version 5.4: /@rushstack/m/1.0.0: + // Version 6.0: /@rushstack/m@1.0.0: + // + // Version 5.4: /@rushstack/j/1.0.0_@rushstack+n@2.0.0 + // Version 6.0: /@rushstack/j@1.0.0(@rushstack/n@2.0.0) + const versionDelimiter: string = pnpmLockfileVersion === 54 ? '/' : '@'; + result.entryId = '/' + result.name + versionDelimiter + result.version; } return new LfxGraphDependency(result); } -// node is the yaml entry that we are trying to parse -function parseDependencies( +// v5.4 used this to parse projects ("importers") also +function parsePackageDependencies( dependencies: LfxGraphDependency[], lockfileEntry: LfxGraphEntry, either: lockfileTypes.ProjectSnapshot | lockfileTypes.PackageSnapshot, - workspace: IJsonLfxWorkspace + pnpmLockfileVersion: PnpmLockfileVersion ): void { const node: Partial = either as unknown as Partial; if (node.dependencies) { - for (const [pkgName, pkgVersion] of Object.entries(node.dependencies)) { + for (const [packageName, version] of Object.entries(node.dependencies)) { dependencies.push( - createPackageLockfileDependency( - pkgName, - pkgVersion, - LfxDependencyKind.Regular, - lockfileEntry, - undefined, - workspace - ) + createPackageLockfileDependency({ + kind: LfxDependencyKind.Regular, + name: packageName, + version: version, + containingEntry: lockfileEntry, + pnpmLockfileVersion + }) ); } } if (node.devDependencies) { - for (const [pkgName, pkgVersion] of Object.entries(node.devDependencies)) { + for (const [packageName, version] of Object.entries(node.devDependencies)) { dependencies.push( - createPackageLockfileDependency( - pkgName, - pkgVersion, - LfxDependencyKind.Dev, - lockfileEntry, - undefined, - workspace - ) + createPackageLockfileDependency({ + kind: LfxDependencyKind.Dev, + name: packageName, + version: version, + containingEntry: lockfileEntry, + pnpmLockfileVersion + }) ); } } if (node.peerDependencies) { - for (const [pkgName, pkgVersion] of Object.entries(node.peerDependencies)) { + for (const [packageName, version] of Object.entries(node.peerDependencies)) { dependencies.push( - createPackageLockfileDependency( - pkgName, - pkgVersion, - LfxDependencyKind.Peer, - lockfileEntry, - node, - workspace - ) + createPackageLockfileDependency({ + kind: LfxDependencyKind.Peer, + name: packageName, + version: version, + containingEntry: lockfileEntry, + peerDependenciesMeta: node.peerDependenciesMeta, + pnpmLockfileVersion + }) ); } } @@ -122,14 +133,47 @@ function parseDependencies( } } +function parseProjectDependencies60( + dependencies: LfxGraphDependency[], + lockfileEntry: LfxGraphEntry, + snapshot: lockfileTypes.LockfileFileProjectSnapshot, + pnpmLockfileVersion: PnpmLockfileVersion +): void { + if (snapshot.dependencies) { + for (const [packageName, specifierAndResolution] of Object.entries(snapshot.dependencies)) { + dependencies.push( + createPackageLockfileDependency({ + kind: LfxDependencyKind.Regular, + name: packageName, + version: specifierAndResolution.version, + containingEntry: lockfileEntry, + pnpmLockfileVersion + }) + ); + } + } + if (snapshot.devDependencies) { + for (const [packageName, specifierAndResolution] of Object.entries(snapshot.devDependencies)) { + dependencies.push( + createPackageLockfileDependency({ + kind: LfxDependencyKind.Dev, + name: packageName, + version: specifierAndResolution.version, + containingEntry: lockfileEntry, + pnpmLockfileVersion + }) + ); + } + } +} + function createProjectLockfileEntry(options: { rawEntryId: string; - rawYamlData: lockfileTypes.ProjectSnapshot; duplicates?: Set; workspace: IJsonLfxWorkspace; pnpmLockfileVersion: PnpmLockfileVersion; }): LfxGraphEntry { - const { rawEntryId, rawYamlData, duplicates, workspace } = options; + const { rawEntryId, duplicates, workspace } = options; const result: ILfxGraphEntryOptions = { kind: LfxGraphEntryKind.Project, @@ -165,7 +209,6 @@ function createProjectLockfileEntry(options: { result.displayText = `Project: ${result.entryPackageName}`; const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); - parseDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData, workspace); return lockfileEntry; } @@ -196,10 +239,13 @@ function createPackageLockfileEntry(options: { result.displayText = rawEntryId; + if (!rawEntryId.startsWith('/')) { + throw new Error('Expecting leading "/" in path: ' + JSON.stringify(rawEntryId)); + } + + let dotPnpmSubfolder: string; + if (pnpmLockfileVersion === 54) { - if (!rawEntryId.startsWith('/')) { - throw new Error('Expecting leading "/" in path: ' + JSON.stringify(rawEntryId)); - } const lastSlashIndex: number = rawEntryId.lastIndexOf('/'); if (lastSlashIndex < 0) { throw new Error('Expecting "/" in path: ' + JSON.stringify(rawEntryId)); @@ -223,14 +269,55 @@ function createPackageLockfileEntry(options: { result.displayText = packageName + ' ' + version; result.entryPackageVersion = version; } - } - // Example: @babel+register@7.17.7_@babel+core@7.17.12 - const dotPnpmSubfolder: string = - result.entryPackageName.replace('/', '+') + - '@' + - result.entryPackageVersion + - (result.entrySuffix ? `_${result.entrySuffix}` : ''); + // Example: @babel+register@7.17.7_@babel+core@7.17.12 + dotPnpmSubfolder = + result.entryPackageName.replace('/', '+') + + '@' + + result.entryPackageVersion + + (result.entrySuffix ? `_${result.entrySuffix}` : ''); + } else { + // /@rushstack/eslint-config@3.0.1 + // --> @rushstack/eslint-config 3.0.1 + + // /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + // --> @rushstack/l 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + let versionAtSignIndex: number; + if (rawEntryId.startsWith('/@')) { + versionAtSignIndex = rawEntryId.indexOf('@', 2); + } else { + versionAtSignIndex = rawEntryId.indexOf('@', 1); + } + const packageName: string = rawEntryId.substring(1, versionAtSignIndex); + result.entryPackageName = packageName; + + const leftParenIndex: number = rawEntryId.indexOf('(', versionAtSignIndex); + if (leftParenIndex < 0) { + const version: string = rawEntryId.substring(versionAtSignIndex + 1); + result.entryPackageVersion = version; + + // @rushstack/eslint-config 3.0.1 + result.displayText = packageName + ' ' + version; + } else { + const version: string = rawEntryId.substring(versionAtSignIndex + 1, leftParenIndex); + result.entryPackageVersion = version; + const suffix: string = rawEntryId.substring(leftParenIndex); + result.entrySuffix = suffix; + + // @rushstack/l 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + result.displayText = packageName + ' ' + version + ' ' + suffix; + } + + // Example: /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + // --> @rushstack+l@1.0.0_@rushstack+m@1.0.0_@rushstack+n@2.0.0 + + // @rushstack/l 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + dotPnpmSubfolder = rawEntryId.substring(1); + dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, '/', '+'); + dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, ')(', '_'); + dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, '(', '_'); + dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, ')', ''); + } // Example: // common/temp/default/node_modules/.pnpm @@ -242,7 +329,7 @@ function createPackageLockfileEntry(options: { ); const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); - parseDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData, workspace); + parsePackageDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData, pnpmLockfileVersion); return lockfileEntry; } @@ -253,7 +340,9 @@ function createPackageLockfileEntry(options: { * @returns A list of all the LockfileEntries in the lockfile. */ export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfxWorkspace): LfxGraph { - const lockfile: lockfileTypes.LockfileObject = lockfileJson as lockfileTypes.LockfileObject; + const lockfile: lockfileTypes.LockfileObject | lockfileTypes.LockfileFile = lockfileJson as + | lockfileTypes.LockfileObject + | lockfileTypes.LockfileFile; let pnpmLockfileVersion: PnpmLockfileVersion; switch (lockfile.lockfileVersion.toString()) { @@ -264,10 +353,10 @@ export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfx case '6.0': pnpmLockfileVersion = 60; break; - case '9': - case '9.0': - pnpmLockfileVersion = 90; - break; + //case '9': + //case '9.0': + // pnpmLockfileVersion = 90; + // break; default: throw new Error('Unsupported PNPM lockfile version ' + JSON.stringify(lockfile.lockfileVersion)); } @@ -300,16 +389,27 @@ export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfx // continue; } - const importerValue: lockfileTypes.ProjectSnapshot = - lockfile.importers[importerKey as pnpmTypes.ProjectId]; - const importer: LfxGraphEntry = createProjectLockfileEntry({ rawEntryId: importerKey, - rawYamlData: importerValue, duplicates, workspace, pnpmLockfileVersion }); + + if (pnpmLockfileVersion === 54) { + const lockfile54: lockfileTypes.LockfileObject = lockfileJson as lockfileTypes.LockfileObject; + const importerValue: lockfileTypes.ProjectSnapshot = + lockfile54.importers[importerKey as pnpmTypes.ProjectId]; + parsePackageDependencies(importer.dependencies, importer, importerValue, pnpmLockfileVersion); + } else { + const lockfile60: lockfileTypes.LockfileFile = lockfileJson as lockfileTypes.LockfileFile; + if (lockfile60.importers) { + const importerValue: lockfileTypes.LockfileFileProjectSnapshot = + lockfile60.importers[importerKey as pnpmTypes.ProjectId]; + parseProjectDependencies60(importer.dependencies, importer, importerValue, pnpmLockfileVersion); + } + } + allImporters.push(importer); allEntries.push(importer); allEntriesById.set(importer.entryId, importer); diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap index a24bbbb326c..c78df3bf754 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap @@ -3,116 +3,116 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` "entries: - dependencies: [] - displayText: '' - entryId: '' - entryPackageName: '' + displayText: 'Project: .' + entryId: project:common/temp + entryPackageName: . entryPackageVersion: '' entrySuffix: '' jsonId: 0 kind: 1 - packageJsonFolderPath: '' + packageJsonFolderPath: common/temp rawEntryId: . referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} resolvedEntryJsonId: 4 version: link:../d displayText: 'Project: a' - entryId: project:./common/projects/a + entryId: project:projects/a entryPackageName: a entryPackageVersion: '' entrySuffix: '' jsonId: 1 kind: 1 - packageJsonFolderPath: ./common/projects/a + packageJsonFolderPath: projects/a rawEntryId: ../../projects/a referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} resolvedEntryJsonId: 4 version: link:../d - dependencyType: regular - entryId: /@rushstack/n/2.0.0 + entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} resolvedEntryJsonId: 10 version: 2.0.0 displayText: 'Project: b' - entryId: project:./common/projects/b + entryId: project:projects/b entryPackageName: b entryPackageVersion: '' entrySuffix: '' jsonId: 2 kind: 1 - packageJsonFolderPath: ./common/projects/b + packageJsonFolderPath: projects/b rawEntryId: ../../projects/b referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/e + entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} resolvedEntryJsonId: 5 version: link:../e - dependencyType: regular - entryId: /@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + entryId: /@rushstack/k@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) name: '@rushstack/k' peerDependencyMeta: {} resolvedEntryJsonId: 7 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - dependencyType: regular - entryId: /@rushstack/m/1.0.0 + entryId: /@rushstack/m@1.0.0 name: '@rushstack/m' peerDependencyMeta: {} resolvedEntryJsonId: 9 version: 1.0.0 displayText: 'Project: c' - entryId: project:./common/projects/c + entryId: project:projects/c entryPackageName: c entryPackageVersion: '' entrySuffix: '' jsonId: 3 kind: 1 - packageJsonFolderPath: ./common/projects/c + packageJsonFolderPath: projects/c rawEntryId: ../../projects/c referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/e + entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} resolvedEntryJsonId: 5 version: link:../e - dependencyType: regular - entryId: /@rushstack/j/1.0.0(@rushstack/n@2.0.0) + entryId: /@rushstack/j@1.0.0(@rushstack/n@2.0.0) name: '@rushstack/j' peerDependencyMeta: {} resolvedEntryJsonId: 6 version: 1.0.0(@rushstack/n@2.0.0) - dependencyType: regular - entryId: /@rushstack/n/2.0.0 + entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} resolvedEntryJsonId: 10 version: 2.0.0 displayText: 'Project: d' - entryId: project:./common/projects/d + entryId: project:projects/d entryPackageName: d entryPackageVersion: '' entrySuffix: '' jsonId: 4 kind: 1 - packageJsonFolderPath: ./common/projects/d + packageJsonFolderPath: projects/d rawEntryId: ../../projects/d referrerJsonIds: - 1 @@ -120,19 +120,19 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: /@rushstack/n/3.0.0 + entryId: /@rushstack/n@3.0.0 name: '@rushstack/n' peerDependencyMeta: {} resolvedEntryJsonId: 11 version: 3.0.0 displayText: 'Project: e' - entryId: project:./common/projects/e + entryId: project:projects/e entryPackageName: e entryPackageVersion: '' entrySuffix: '' jsonId: 5 kind: 1 - packageJsonFolderPath: ./common/projects/e + packageJsonFolderPath: projects/e rawEntryId: ../../projects/e referrerJsonIds: - 3 @@ -140,48 +140,47 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: /@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + entryId: /@rushstack/k@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) name: '@rushstack/k' peerDependencyMeta: {} resolvedEntryJsonId: 7 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - dependencyType: regular - entryId: /@rushstack/m/1.0.0 + entryId: /@rushstack/m@1.0.0 name: '@rushstack/m' peerDependencyMeta: {} resolvedEntryJsonId: 9 version: 1.0.0 - displayText: '@rushstack/j/1.0.0(@rushstack n@2.0.0)' + displayText: '@rushstack/j 1.0.0 (@rushstack/n@2.0.0)' entryId: '' - entryPackageName: '@rushstack/j/1.0.0(@rushstack' - entryPackageVersion: n@2.0.0) - entrySuffix: '' + entryPackageName: '@rushstack/j' + entryPackageVersion: 1.0.0 + entrySuffix: (@rushstack/n@2.0.0) jsonId: 6 kind: 2 - packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+j/1.0.0(@rushstack@n@2.0.0)/node_modules/@rushstack/j/1.0.0(@rushstack - rawEntryId: /@rushstack/j/1.0.0(@rushstack/n@2.0.0) + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+j@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/j + rawEntryId: /@rushstack/j@1.0.0(@rushstack/n@2.0.0) referrerJsonIds: - 4 transitivePeerDependencies: - '@rushstack/n' - dependencies: - dependencyType: regular - entryId: /@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + entryId: /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) name: '@rushstack/l' peerDependencyMeta: {} resolvedEntryJsonId: 8 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - displayText: '@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack n@2.0.0)' + displayText: '@rushstack/k 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0)' entryId: '' - entryPackageName: '@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack' - entryPackageVersion: n@2.0.0) - entrySuffix: '' + entryPackageName: '@rushstack/k' + entryPackageVersion: 1.0.0 + entrySuffix: (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) jsonId: 7 kind: 2 packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+k/1.0.0(@rushstack/m@1.0.0)(@rushstack@n@2.0.0)/node_modules/@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack - rawEntryId: /@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + common/temp/node_modules/.pnpm/@rushstack+k@1.0.0_@rushstack+m@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/k + rawEntryId: /@rushstack/k@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) referrerJsonIds: - 3 - 6 @@ -190,13 +189,13 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` - '@rushstack/n' - dependencies: - dependencyType: regular - entryId: /@rushstack/m/1.0.0 + entryId: /@rushstack/m@1.0.0 name: '@rushstack/m' peerDependencyMeta: {} resolvedEntryJsonId: 9 version: 1.0.0 - dependencyType: regular - entryId: /@rushstack/n/2.0.0 + entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} resolvedEntryJsonId: 10 @@ -217,16 +216,16 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` optional: true version: ^2.0.0 version: ^2.0.0 - displayText: '@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack n@2.0.0)' + displayText: '@rushstack/l 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0)' entryId: '' - entryPackageName: '@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack' - entryPackageVersion: n@2.0.0) - entrySuffix: '' + entryPackageName: '@rushstack/l' + entryPackageVersion: 1.0.0 + entrySuffix: (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) jsonId: 8 kind: 2 packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+l/1.0.0(@rushstack/m@1.0.0)(@rushstack@n@2.0.0)/node_modules/@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack - rawEntryId: /@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + common/temp/node_modules/.pnpm/@rushstack+l@1.0.0_@rushstack+m@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/l + rawEntryId: /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) referrerJsonIds: - 7 transitivePeerDependencies: [] @@ -238,8 +237,8 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entrySuffix: '' jsonId: 9 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m - rawEntryId: /@rushstack/m/1.0.0 + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m + rawEntryId: /@rushstack/m@1.0.0 referrerJsonIds: - 3 - 6 @@ -253,8 +252,8 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entrySuffix: '' jsonId: 10 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n - rawEntryId: /@rushstack/n/2.0.0 + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n + rawEntryId: /@rushstack/n@2.0.0 referrerJsonIds: - 2 - 4 @@ -268,16 +267,17 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entrySuffix: '' jsonId: 11 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n - rawEntryId: /@rushstack/n/3.0.0 + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n + rawEntryId: /@rushstack/n@3.0.0 referrerJsonIds: - 5 transitivePeerDependencies: [] workspace: + pnpmLockfileFolder: common/temp pnpmLockfilePath: common/temp/pnpm-lock.yaml rushConfig: rushVersion: 5.158.1 subspaceName: '' - workspaceRootFolder: /repo + workspaceRootFullPath: /repo " `; From e65d83e6858e9725134939e8e6cf292d3a717a0e Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 03:44:30 -0700 Subject: [PATCH 09/17] Hide the "." package from Rush workspaces, shuffling the jsonId's in all the snapshots --- .../cli/explorer/ExplorerCommandLineParser.ts | 6 +- .../src/graph/lfxGraphLoader.ts | 2 +- ...fxGraph-website-sample-1-v5.4.test.ts.snap | 100 ++++++++---------- ...fxGraph-website-sample-1-v6.0.test.ts.snap | 90 +++++++--------- .../src/graph/test/serializeToJson.test.ts | 28 ++--- 5 files changed, 92 insertions(+), 134 deletions(-) diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index c4012c14b9e..cf0bd6e7839 100644 --- a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -153,11 +153,7 @@ export class ExplorerCommandLineParser extends CommandLineParser { const pnpmLockfileText: string = await FileSystem.readFileAsync(appState.pnpmLockfileLocation); const lockfile: unknown = yaml.load(pnpmLockfileText) as unknown; - const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph( - appState.lfxWorkspace, - lockfile, - appState.lfxWorkspace.rushConfig?.subspaceName ?? '' - ); + const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(lockfile, appState.lfxWorkspace); const jsonGraph: IJsonLfxGraph = lfxGraphSerializer.serializeToJson(graph); res.type('application/json').send(jsonGraph); diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index ab7cb7d9540..cdb14e6455a 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -386,7 +386,7 @@ export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfx for (const importerKey of Object.keys(lockfile.importers)) { if (isRushWorkspace && importerKey === '.') { // Discard the synthetic package.json file created by Rush under common/temp - // continue; + continue; } const importer: LfxGraphEntry = createProjectLockfileEntry({ diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap index 362b1874547..a79208cc8c9 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap @@ -2,31 +2,19 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` "entries: - - dependencies: [] - displayText: 'Project: .' - entryId: project:common/temp - entryPackageName: . - entryPackageVersion: '' - entrySuffix: '' - jsonId: 0 - kind: 1 - packageJsonFolderPath: common/temp - rawEntryId: . - referrerJsonIds: [] - transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d displayText: 'Project: a' entryId: project:projects/a entryPackageName: a entryPackageVersion: '' entrySuffix: '' - jsonId: 1 + jsonId: 0 kind: 1 packageJsonFolderPath: projects/a rawEntryId: ../../projects/a @@ -37,20 +25,20 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d - dependencyType: regular entryId: /@rushstack/n/2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 12 + resolvedEntryJsonId: 11 version: 2.0.0 displayText: 'Project: b' entryId: project:projects/b entryPackageName: b entryPackageVersion: '' entrySuffix: '' - jsonId: 2 + jsonId: 1 kind: 1 packageJsonFolderPath: projects/b rawEntryId: ../../projects/b @@ -61,26 +49,26 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} - resolvedEntryJsonId: 5 + resolvedEntryJsonId: 4 version: link:../e - dependencyType: regular entryId: /@rushstack/k/1.0.0_@rushstack+m@1.0.0 name: '@rushstack/k' peerDependencyMeta: {} - resolvedEntryJsonId: 7 + resolvedEntryJsonId: 6 version: 1.0.0_@rushstack+m@1.0.0 - dependencyType: regular entryId: /@rushstack/m/1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 1.0.0 displayText: 'Project: c' entryId: project:projects/c entryPackageName: c entryPackageVersion: '' entrySuffix: '' - jsonId: 3 + jsonId: 2 kind: 1 packageJsonFolderPath: projects/c rawEntryId: ../../projects/c @@ -91,77 +79,77 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} - resolvedEntryJsonId: 5 + resolvedEntryJsonId: 4 version: link:../e - dependencyType: regular entryId: /@rushstack/j/1.0.0_@rushstack+n@2.0.0 name: '@rushstack/j' peerDependencyMeta: {} - resolvedEntryJsonId: 6 + resolvedEntryJsonId: 5 version: 1.0.0_@rushstack+n@2.0.0 - dependencyType: regular entryId: /@rushstack/n/2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 12 + resolvedEntryJsonId: 11 version: 2.0.0 displayText: 'Project: d' entryId: project:projects/d entryPackageName: d entryPackageVersion: '' entrySuffix: '' - jsonId: 4 + jsonId: 3 kind: 1 packageJsonFolderPath: projects/d rawEntryId: ../../projects/d referrerJsonIds: + - 0 - 1 - - 2 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /@rushstack/n/3.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 13 + resolvedEntryJsonId: 12 version: 3.0.0 displayText: 'Project: e' entryId: project:projects/e entryPackageName: e entryPackageVersion: '' entrySuffix: '' - jsonId: 5 + jsonId: 4 kind: 1 packageJsonFolderPath: projects/e rawEntryId: ../../projects/e referrerJsonIds: + - 2 - 3 - - 4 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /@rushstack/k/1.0.0_wxpgugna4ivthu7yyu4fmciltu name: '@rushstack/k' peerDependencyMeta: {} - resolvedEntryJsonId: 8 + resolvedEntryJsonId: 7 version: 1.0.0_wxpgugna4ivthu7yyu4fmciltu - dependencyType: regular entryId: /@rushstack/m/1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 1.0.0 displayText: '@rushstack/j 1.0.0 (@rushstack+n@2.0.0)' entryId: '' entryPackageName: '@rushstack/j' entryPackageVersion: 1.0.0 entrySuffix: '@rushstack+n@2.0.0' - jsonId: 6 + jsonId: 5 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+j@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/j rawEntryId: /@rushstack/j/1.0.0_@rushstack+n@2.0.0 referrerJsonIds: - - 4 + - 3 transitivePeerDependencies: - '@rushstack/n' - dependencies: @@ -169,19 +157,19 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryId: /@rushstack/l/1.0.0_@rushstack+m@1.0.0 name: '@rushstack/l' peerDependencyMeta: {} - resolvedEntryJsonId: 9 + resolvedEntryJsonId: 8 version: 1.0.0_@rushstack+m@1.0.0 displayText: '@rushstack/k 1.0.0 (@rushstack+m@1.0.0)' entryId: '' entryPackageName: '@rushstack/k' entryPackageVersion: 1.0.0 entrySuffix: '@rushstack+m@1.0.0' - jsonId: 7 + jsonId: 6 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+k@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/k rawEntryId: /@rushstack/k/1.0.0_@rushstack+m@1.0.0 referrerJsonIds: - - 3 + - 2 transitivePeerDependencies: - '@rushstack/m' - '@rushstack/n' @@ -190,19 +178,19 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryId: /@rushstack/l/1.0.0_wxpgugna4ivthu7yyu4fmciltu name: '@rushstack/l' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 1.0.0_wxpgugna4ivthu7yyu4fmciltu displayText: '@rushstack/k 1.0.0 (wxpgugna4ivthu7yyu4fmciltu)' entryId: '' entryPackageName: '@rushstack/k' entryPackageVersion: 1.0.0 entrySuffix: wxpgugna4ivthu7yyu4fmciltu - jsonId: 8 + jsonId: 7 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+k@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/k rawEntryId: /@rushstack/k/1.0.0_wxpgugna4ivthu7yyu4fmciltu referrerJsonIds: - - 6 + - 5 transitivePeerDependencies: - '@rushstack/m' - '@rushstack/n' @@ -211,7 +199,7 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryId: /@rushstack/m/1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 1.0.0 - dependencyType: peer entryId: 'Peer: @rushstack/m' @@ -234,25 +222,25 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/l' entryPackageVersion: 1.0.0 entrySuffix: '@rushstack+m@1.0.0' - jsonId: 9 + jsonId: 8 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+l@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/l rawEntryId: /@rushstack/l/1.0.0_@rushstack+m@1.0.0 referrerJsonIds: - - 7 + - 6 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /@rushstack/m/1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 1.0.0 - dependencyType: regular entryId: /@rushstack/n/2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 12 + resolvedEntryJsonId: 11 version: 2.0.0 - dependencyType: peer entryId: 'Peer: @rushstack/m' @@ -275,12 +263,12 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/l' entryPackageVersion: 1.0.0 entrySuffix: wxpgugna4ivthu7yyu4fmciltu - jsonId: 10 + jsonId: 9 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+l@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/l rawEntryId: /@rushstack/l/1.0.0_wxpgugna4ivthu7yyu4fmciltu referrerJsonIds: - - 8 + - 7 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/m 1.0.0' @@ -288,15 +276,15 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/m' entryPackageVersion: 1.0.0 entrySuffix: '' - jsonId: 11 + jsonId: 10 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m rawEntryId: /@rushstack/m/1.0.0 referrerJsonIds: - - 3 - - 6 + - 2 + - 5 + - 8 - 9 - - 10 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 2.0.0' @@ -304,14 +292,14 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/n' entryPackageVersion: 2.0.0 entrySuffix: '' - jsonId: 12 + jsonId: 11 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n rawEntryId: /@rushstack/n/2.0.0 referrerJsonIds: - - 2 - - 4 - - 10 + - 1 + - 3 + - 9 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 3.0.0' @@ -319,12 +307,12 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/n' entryPackageVersion: 3.0.0 entrySuffix: '' - jsonId: 13 + jsonId: 12 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n rawEntryId: /@rushstack/n/3.0.0 referrerJsonIds: - - 5 + - 4 transitivePeerDependencies: [] workspace: pnpmLockfileFolder: common/temp diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap index c78df3bf754..9b9d7929861 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap @@ -2,31 +2,19 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` "entries: - - dependencies: [] - displayText: 'Project: .' - entryId: project:common/temp - entryPackageName: . - entryPackageVersion: '' - entrySuffix: '' - jsonId: 0 - kind: 1 - packageJsonFolderPath: common/temp - rawEntryId: . - referrerJsonIds: [] - transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d displayText: 'Project: a' entryId: project:projects/a entryPackageName: a entryPackageVersion: '' entrySuffix: '' - jsonId: 1 + jsonId: 0 kind: 1 packageJsonFolderPath: projects/a rawEntryId: ../../projects/a @@ -37,20 +25,20 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d - dependencyType: regular entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 2.0.0 displayText: 'Project: b' entryId: project:projects/b entryPackageName: b entryPackageVersion: '' entrySuffix: '' - jsonId: 2 + jsonId: 1 kind: 1 packageJsonFolderPath: projects/b rawEntryId: ../../projects/b @@ -61,26 +49,26 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} - resolvedEntryJsonId: 5 + resolvedEntryJsonId: 4 version: link:../e - dependencyType: regular entryId: /@rushstack/k@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) name: '@rushstack/k' peerDependencyMeta: {} - resolvedEntryJsonId: 7 + resolvedEntryJsonId: 6 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - dependencyType: regular entryId: /@rushstack/m@1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 9 + resolvedEntryJsonId: 8 version: 1.0.0 displayText: 'Project: c' entryId: project:projects/c entryPackageName: c entryPackageVersion: '' entrySuffix: '' - jsonId: 3 + jsonId: 2 kind: 1 packageJsonFolderPath: projects/c rawEntryId: ../../projects/c @@ -91,77 +79,77 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} - resolvedEntryJsonId: 5 + resolvedEntryJsonId: 4 version: link:../e - dependencyType: regular entryId: /@rushstack/j@1.0.0(@rushstack/n@2.0.0) name: '@rushstack/j' peerDependencyMeta: {} - resolvedEntryJsonId: 6 + resolvedEntryJsonId: 5 version: 1.0.0(@rushstack/n@2.0.0) - dependencyType: regular entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 2.0.0 displayText: 'Project: d' entryId: project:projects/d entryPackageName: d entryPackageVersion: '' entrySuffix: '' - jsonId: 4 + jsonId: 3 kind: 1 packageJsonFolderPath: projects/d rawEntryId: ../../projects/d referrerJsonIds: + - 0 - 1 - - 2 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /@rushstack/n@3.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 3.0.0 displayText: 'Project: e' entryId: project:projects/e entryPackageName: e entryPackageVersion: '' entrySuffix: '' - jsonId: 5 + jsonId: 4 kind: 1 packageJsonFolderPath: projects/e rawEntryId: ../../projects/e referrerJsonIds: + - 2 - 3 - - 4 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /@rushstack/k@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) name: '@rushstack/k' peerDependencyMeta: {} - resolvedEntryJsonId: 7 + resolvedEntryJsonId: 6 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - dependencyType: regular entryId: /@rushstack/m@1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 9 + resolvedEntryJsonId: 8 version: 1.0.0 displayText: '@rushstack/j 1.0.0 (@rushstack/n@2.0.0)' entryId: '' entryPackageName: '@rushstack/j' entryPackageVersion: 1.0.0 entrySuffix: (@rushstack/n@2.0.0) - jsonId: 6 + jsonId: 5 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+j@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/j rawEntryId: /@rushstack/j@1.0.0(@rushstack/n@2.0.0) referrerJsonIds: - - 4 + - 3 transitivePeerDependencies: - '@rushstack/n' - dependencies: @@ -169,21 +157,21 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryId: /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) name: '@rushstack/l' peerDependencyMeta: {} - resolvedEntryJsonId: 8 + resolvedEntryJsonId: 7 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) displayText: '@rushstack/k 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0)' entryId: '' entryPackageName: '@rushstack/k' entryPackageVersion: 1.0.0 entrySuffix: (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - jsonId: 7 + jsonId: 6 kind: 2 packageJsonFolderPath: >- common/temp/node_modules/.pnpm/@rushstack+k@1.0.0_@rushstack+m@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/k rawEntryId: /@rushstack/k@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) referrerJsonIds: - - 3 - - 6 + - 2 + - 5 transitivePeerDependencies: - '@rushstack/m' - '@rushstack/n' @@ -192,13 +180,13 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryId: /@rushstack/m@1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 9 + resolvedEntryJsonId: 8 version: 1.0.0 - dependencyType: regular entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 2.0.0 - dependencyType: peer entryId: 'Peer: @rushstack/m' @@ -221,13 +209,13 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryPackageName: '@rushstack/l' entryPackageVersion: 1.0.0 entrySuffix: (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - jsonId: 8 + jsonId: 7 kind: 2 packageJsonFolderPath: >- common/temp/node_modules/.pnpm/@rushstack+l@1.0.0_@rushstack+m@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/l rawEntryId: /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) referrerJsonIds: - - 7 + - 6 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/m 1.0.0' @@ -235,14 +223,14 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryPackageName: '@rushstack/m' entryPackageVersion: 1.0.0 entrySuffix: '' - jsonId: 9 + jsonId: 8 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m rawEntryId: /@rushstack/m@1.0.0 referrerJsonIds: - - 3 - - 6 - - 8 + - 2 + - 5 + - 7 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 2.0.0' @@ -250,14 +238,14 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryPackageName: '@rushstack/n' entryPackageVersion: 2.0.0 entrySuffix: '' - jsonId: 10 + jsonId: 9 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n rawEntryId: /@rushstack/n@2.0.0 referrerJsonIds: - - 2 - - 4 - - 8 + - 1 + - 3 + - 7 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 3.0.0' @@ -265,12 +253,12 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryPackageName: '@rushstack/n' entryPackageVersion: 3.0.0 entrySuffix: '' - jsonId: 11 + jsonId: 10 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n rawEntryId: /@rushstack/n@3.0.0 referrerJsonIds: - - 5 + - 4 transitivePeerDependencies: [] workspace: pnpmLockfileFolder: common/temp diff --git a/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts b/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts index 493ee2ca5e4..46ffc2c09db 100644 --- a/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts +++ b/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts @@ -13,20 +13,6 @@ describe('serializeToJson', () => { expect(lfxGraphSerializer.serializeToJson(graph)).toMatchInlineSnapshot(` Object { "entries": Array [ - Object { - "dependencies": Array [], - "displayText": "Project: .", - "entryId": "project:common/temp/my-subspace", - "entryPackageName": ".", - "entryPackageVersion": "", - "entrySuffix": "", - "jsonId": 0, - "kind": 1, - "packageJsonFolderPath": "common/temp/my-subspace", - "rawEntryId": ".", - "referrerJsonIds": Array [], - "transitivePeerDependencies": Array [], - }, Object { "dependencies": Array [ Object { @@ -38,7 +24,7 @@ Object { "optional": undefined, "version": undefined, }, - "resolvedEntryJsonId": 2, + "resolvedEntryJsonId": 1, "version": "1.7.1", }, Object { @@ -50,7 +36,7 @@ Object { "optional": undefined, "version": undefined, }, - "resolvedEntryJsonId": 3, + "resolvedEntryJsonId": 2, "version": "1.7.1", }, ], @@ -59,7 +45,7 @@ Object { "entryPackageName": "testApp1", "entryPackageVersion": "", "entrySuffix": "", - "jsonId": 1, + "jsonId": 0, "kind": 1, "packageJsonFolderPath": "apps/testApp1", "rawEntryId": "../../../apps/testApp1", @@ -73,12 +59,12 @@ Object { "entryPackageName": "@testPackage/core", "entryPackageVersion": "1.7.1", "entrySuffix": "", - "jsonId": 2, + "jsonId": 1, "kind": 2, "packageJsonFolderPath": "common/temp/my-subspace/node_modules/.pnpm/@testPackage+core@1.7.1/node_modules/@testPackage/core", "rawEntryId": "/@testPackage/core/1.7.1", "referrerJsonIds": Array [ - 1, + 0, ], "transitivePeerDependencies": Array [], }, @@ -89,12 +75,12 @@ Object { "entryPackageName": "@testPackage2/core", "entryPackageVersion": "1.7.1", "entrySuffix": "", - "jsonId": 3, + "jsonId": 2, "kind": 2, "packageJsonFolderPath": "common/temp/my-subspace/node_modules/.pnpm/@testPackage2+core@1.7.1/node_modules/@testPackage2/core", "rawEntryId": "/@testPackage2/core/1.7.1", "referrerJsonIds": Array [ - 1, + 0, ], "transitivePeerDependencies": Array [], }, From 03ef2a0a19855ef5da9a665fe4ec85c0fdbfc102 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 03:48:00 -0700 Subject: [PATCH 10/17] Fix an incorrect path --- apps/lockfile-explorer/src/utils/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/lockfile-explorer/src/utils/init.ts b/apps/lockfile-explorer/src/utils/init.ts index dd30057b477..55f21779109 100644 --- a/apps/lockfile-explorer/src/utils/init.ts +++ b/apps/lockfile-explorer/src/utils/init.ts @@ -46,7 +46,7 @@ export const init = (options: { lfxWorkspace: { workspaceRootFullPath: currentFolder, pnpmLockfilePath: Path.convertToSlashes(pnpmLockfileRelativePath), - pnpmLockfileFolder: Path.convertToSlashes(path.basename(pnpmLockfileRelativePath)), + pnpmLockfileFolder: Path.convertToSlashes(path.dirname(pnpmLockfileRelativePath)), rushConfig: { rushVersion: rushConfiguration.rushConfigurationJson.rushVersion, subspaceName: subspaceName ?? '' From 660d4d1da047765c8c11646396a33a485b447529 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 04:01:37 -0700 Subject: [PATCH 11/17] rush change --- .../octogonz-lfx-fixes3_2025-09-16-11-01.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes3_2025-09-16-11-01.json diff --git a/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes3_2025-09-16-11-01.json b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes3_2025-09-16-11-01.json new file mode 100644 index 00000000000..7b288ff1b92 --- /dev/null +++ b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes3_2025-09-16-11-01.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/lockfile-explorer", + "comment": "Improve support for PNPM lockfile format V6.0", + "type": "patch" + } + ], + "packageName": "@rushstack/lockfile-explorer" +} \ No newline at end of file From 1161d48703289b62e6082284155bac3ace7fc150 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 04:10:28 -0700 Subject: [PATCH 12/17] Improve formatting of 6.0 suffixes --- .../src/graph/lfxGraphLoader.ts | 22 +++++++++++++------ ...fxGraph-website-sample-1-v6.0.test.ts.snap | 12 +++++----- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index cdb14e6455a..7dfd04e093d 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -277,11 +277,9 @@ function createPackageLockfileEntry(options: { result.entryPackageVersion + (result.entrySuffix ? `_${result.entrySuffix}` : ''); } else { + // Example inputs: // /@rushstack/eslint-config@3.0.1 - // --> @rushstack/eslint-config 3.0.1 - // /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - // --> @rushstack/l 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) let versionAtSignIndex: number; if (rawEntryId.startsWith('/@')) { versionAtSignIndex = rawEntryId.indexOf('@', 2); @@ -296,16 +294,26 @@ function createPackageLockfileEntry(options: { const version: string = rawEntryId.substring(versionAtSignIndex + 1); result.entryPackageVersion = version; - // @rushstack/eslint-config 3.0.1 + // /@rushstack/eslint-config@3.0.1 + // --> @rushstack/eslint-config 3.0.1 result.displayText = packageName + ' ' + version; } else { const version: string = rawEntryId.substring(versionAtSignIndex + 1, leftParenIndex); result.entryPackageVersion = version; - const suffix: string = rawEntryId.substring(leftParenIndex); + + // "(@rushstack/m@1.0.0)(@rushstack/n@2.0.0)" + let suffix: string = rawEntryId.substring(leftParenIndex); + + // Rewrite to: + // "@rushstack/m@1.0.0; @rushstack/n@2.0.0" + suffix = Text.replaceAll(suffix, ')(', '; '); + suffix = Text.replaceAll(suffix, '(', ''); + suffix = Text.replaceAll(suffix, ')', ''); result.entrySuffix = suffix; - // @rushstack/l 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - result.displayText = packageName + ' ' + version + ' ' + suffix; + // /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + // --> @rushstack/l 1.0.0 [@rushstack/m@1.0.0; @rushstack/n@2.0.0] + result.displayText = packageName + ' ' + version + ' [' + suffix + ']'; } // Example: /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap index 9b9d7929861..7f65a88ff52 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap @@ -139,11 +139,11 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` peerDependencyMeta: {} resolvedEntryJsonId: 8 version: 1.0.0 - displayText: '@rushstack/j 1.0.0 (@rushstack/n@2.0.0)' + displayText: '@rushstack/j 1.0.0 [@rushstack/n@2.0.0]' entryId: '' entryPackageName: '@rushstack/j' entryPackageVersion: 1.0.0 - entrySuffix: (@rushstack/n@2.0.0) + entrySuffix: '@rushstack/n@2.0.0' jsonId: 5 kind: 2 packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+j@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/j @@ -159,11 +159,11 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` peerDependencyMeta: {} resolvedEntryJsonId: 7 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - displayText: '@rushstack/k 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0)' + displayText: '@rushstack/k 1.0.0 [@rushstack/m@1.0.0; @rushstack/n@2.0.0]' entryId: '' entryPackageName: '@rushstack/k' entryPackageVersion: 1.0.0 - entrySuffix: (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + entrySuffix: '@rushstack/m@1.0.0; @rushstack/n@2.0.0' jsonId: 6 kind: 2 packageJsonFolderPath: >- @@ -204,11 +204,11 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` optional: true version: ^2.0.0 version: ^2.0.0 - displayText: '@rushstack/l 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0)' + displayText: '@rushstack/l 1.0.0 [@rushstack/m@1.0.0; @rushstack/n@2.0.0]' entryId: '' entryPackageName: '@rushstack/l' entryPackageVersion: 1.0.0 - entrySuffix: (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + entrySuffix: '@rushstack/m@1.0.0; @rushstack/n@2.0.0' jsonId: 7 kind: 2 packageJsonFolderPath: >- From 26189c4acd89a709a39c9003f01615a072cbeb3b Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 04:18:01 -0700 Subject: [PATCH 13/17] Move entry to nonbrowser-approved-packages.json --- common/config/rush/browser-approved-packages.json | 4 ---- common/config/rush/nonbrowser-approved-packages.json | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index bf68568ed36..15e248ddeba 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -14,10 +14,6 @@ "name": "@lifaon/path", "allowedCategories": [ "libraries" ] }, - { - "name": "@pnpm/types", - "allowedCategories": [ "libraries" ] - }, { "name": "@radix-ui/colors", "allowedCategories": [ "libraries" ] diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 9f344afc501..b2dde4e55ed 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -114,6 +114,10 @@ "name": "@pnpm/logger", "allowedCategories": [ "libraries" ] }, + { + "name": "@pnpm/types", + "allowedCategories": [ "libraries" ] + }, { "name": "@redis/client", "allowedCategories": [ "libraries" ] From c3b731c864160f9eefe9c67d54bec9577a01e182 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:36:18 -0700 Subject: [PATCH 14/17] Add a failing test case for "link:" in packages section --- .../lfxGraph-edge-cases-v5.4.test.ts.snap | 69 ++++++-- .../lfxGraph-edge-cases-v6.0.test.ts.snap | 165 ++++++++++++++++++ .../fixtures/edge-cases/pnpm-lock-v5.4.yaml | 18 ++ .../fixtures/edge-cases/pnpm-lock-v6.0.yaml | 77 ++++++++ .../test/lfxGraph-edge-cases-v6.0.test.ts | 23 +++ 5 files changed, 338 insertions(+), 14 deletions(-) create mode 100644 apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap create mode 100644 apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v6.0.yaml create mode 100644 apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v6.0.test.ts diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap index 3c19dff2d8a..37db52aea0e 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap @@ -7,7 +7,7 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` entryId: /color/5.0.2 name: color peerDependencyMeta: {} - resolvedEntryJsonId: 5 + resolvedEntryJsonId: 6 version: 5.0.2 displayText: 'Project: duplicate (duplicate-1/duplicate)' entryId: project:duplicate-1/duplicate @@ -25,7 +25,7 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` entryId: /color-string/2.1.2 name: color-string peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 5 version: 2.1.2 displayText: 'Project: duplicate (duplicate-2/duplicate)' entryId: project:duplicate-2/duplicate @@ -38,24 +38,47 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` rawEntryId: duplicate-2/duplicate referrerJsonIds: [] transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /has-symbols/1.0.2 + name: has-symbols + peerDependencyMeta: {} + resolvedEntryJsonId: 7 + version: 1.0.2 + - dependencyType: regular + entryId: project:link-specifier/target-folder + name: target-folder + peerDependencyMeta: {} + version: link:../target-folder + displayText: 'Project: linker' + entryId: project:link-specifier/linker + entryPackageName: linker + entryPackageVersion: '' + entrySuffix: '' + jsonId: 2 + kind: 1 + packageJsonFolderPath: link-specifier/linker + rawEntryId: link-specifier/linker + referrerJsonIds: [] + transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /color-name/2.0.2 name: color-name peerDependencyMeta: {} - resolvedEntryJsonId: 3 + resolvedEntryJsonId: 4 version: 2.0.2 displayText: color-convert 3.1.2 entryId: '' entryPackageName: color-convert entryPackageVersion: 3.1.2 entrySuffix: '' - jsonId: 2 + jsonId: 3 kind: 2 packageJsonFolderPath: node_modules/.pnpm/color-convert@3.1.2/node_modules/color-convert rawEntryId: /color-convert/3.1.2 referrerJsonIds: - - 5 + - 6 transitivePeerDependencies: [] - dependencies: [] displayText: color-name 2.0.2 @@ -63,59 +86,77 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` entryPackageName: color-name entryPackageVersion: 2.0.2 entrySuffix: '' - jsonId: 3 + jsonId: 4 kind: 2 packageJsonFolderPath: node_modules/.pnpm/color-name@2.0.2/node_modules/color-name rawEntryId: /color-name/2.0.2 referrerJsonIds: - - 2 - - 4 + - 3 + - 5 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /color-name/2.0.2 name: color-name peerDependencyMeta: {} - resolvedEntryJsonId: 3 + resolvedEntryJsonId: 4 version: 2.0.2 displayText: color-string 2.1.2 entryId: '' entryPackageName: color-string entryPackageVersion: 2.1.2 entrySuffix: '' - jsonId: 4 + jsonId: 5 kind: 2 packageJsonFolderPath: node_modules/.pnpm/color-string@2.1.2/node_modules/color-string rawEntryId: /color-string/2.1.2 referrerJsonIds: - 1 - - 5 + - 6 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /color-convert/3.1.2 name: color-convert peerDependencyMeta: {} - resolvedEntryJsonId: 2 + resolvedEntryJsonId: 3 version: 3.1.2 - dependencyType: regular entryId: /color-string/2.1.2 name: color-string peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 5 version: 2.1.2 displayText: color 5.0.2 entryId: '' entryPackageName: color entryPackageVersion: 5.0.2 entrySuffix: '' - jsonId: 5 + jsonId: 6 kind: 2 packageJsonFolderPath: node_modules/.pnpm/color@5.0.2/node_modules/color rawEntryId: /color/5.0.2 referrerJsonIds: - 0 transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: project:node_modules/.pnpm/has-symbols@1.0.2/node_modules/has-symbols/link-specifier/target-folder + name: target-folder + peerDependencyMeta: {} + version: link:link-specifier/target-folder + displayText: has-symbols 1.0.2 + entryId: '' + entryPackageName: has-symbols + entryPackageVersion: 1.0.2 + entrySuffix: '' + jsonId: 7 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/has-symbols@1.0.2/node_modules/has-symbols + rawEntryId: /has-symbols/1.0.2 + referrerJsonIds: + - 2 + transitivePeerDependencies: [] workspace: pnpmLockfileFolder: '' pnpmLockfilePath: pnpm-lock.yaml diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap new file mode 100644 index 00000000000..37db52aea0e --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap @@ -0,0 +1,165 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` +"entries: + - dependencies: + - dependencyType: regular + entryId: /color/5.0.2 + name: color + peerDependencyMeta: {} + resolvedEntryJsonId: 6 + version: 5.0.2 + displayText: 'Project: duplicate (duplicate-1/duplicate)' + entryId: project:duplicate-1/duplicate + entryPackageName: duplicate (duplicate-1/duplicate) + entryPackageVersion: '' + entrySuffix: '' + jsonId: 0 + kind: 1 + packageJsonFolderPath: duplicate-1/duplicate + rawEntryId: duplicate-1/duplicate + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-string/2.1.2 + name: color-string + peerDependencyMeta: {} + resolvedEntryJsonId: 5 + version: 2.1.2 + displayText: 'Project: duplicate (duplicate-2/duplicate)' + entryId: project:duplicate-2/duplicate + entryPackageName: duplicate (duplicate-2/duplicate) + entryPackageVersion: '' + entrySuffix: '' + jsonId: 1 + kind: 1 + packageJsonFolderPath: duplicate-2/duplicate + rawEntryId: duplicate-2/duplicate + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /has-symbols/1.0.2 + name: has-symbols + peerDependencyMeta: {} + resolvedEntryJsonId: 7 + version: 1.0.2 + - dependencyType: regular + entryId: project:link-specifier/target-folder + name: target-folder + peerDependencyMeta: {} + version: link:../target-folder + displayText: 'Project: linker' + entryId: project:link-specifier/linker + entryPackageName: linker + entryPackageVersion: '' + entrySuffix: '' + jsonId: 2 + kind: 1 + packageJsonFolderPath: link-specifier/linker + rawEntryId: link-specifier/linker + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-name/2.0.2 + name: color-name + peerDependencyMeta: {} + resolvedEntryJsonId: 4 + version: 2.0.2 + displayText: color-convert 3.1.2 + entryId: '' + entryPackageName: color-convert + entryPackageVersion: 3.1.2 + entrySuffix: '' + jsonId: 3 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color-convert@3.1.2/node_modules/color-convert + rawEntryId: /color-convert/3.1.2 + referrerJsonIds: + - 6 + transitivePeerDependencies: [] + - dependencies: [] + displayText: color-name 2.0.2 + entryId: '' + entryPackageName: color-name + entryPackageVersion: 2.0.2 + entrySuffix: '' + jsonId: 4 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color-name@2.0.2/node_modules/color-name + rawEntryId: /color-name/2.0.2 + referrerJsonIds: + - 3 + - 5 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-name/2.0.2 + name: color-name + peerDependencyMeta: {} + resolvedEntryJsonId: 4 + version: 2.0.2 + displayText: color-string 2.1.2 + entryId: '' + entryPackageName: color-string + entryPackageVersion: 2.1.2 + entrySuffix: '' + jsonId: 5 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color-string@2.1.2/node_modules/color-string + rawEntryId: /color-string/2.1.2 + referrerJsonIds: + - 1 + - 6 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-convert/3.1.2 + name: color-convert + peerDependencyMeta: {} + resolvedEntryJsonId: 3 + version: 3.1.2 + - dependencyType: regular + entryId: /color-string/2.1.2 + name: color-string + peerDependencyMeta: {} + resolvedEntryJsonId: 5 + version: 2.1.2 + displayText: color 5.0.2 + entryId: '' + entryPackageName: color + entryPackageVersion: 5.0.2 + entrySuffix: '' + jsonId: 6 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color@5.0.2/node_modules/color + rawEntryId: /color/5.0.2 + referrerJsonIds: + - 0 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: project:node_modules/.pnpm/has-symbols@1.0.2/node_modules/has-symbols/link-specifier/target-folder + name: target-folder + peerDependencyMeta: {} + version: link:link-specifier/target-folder + displayText: has-symbols 1.0.2 + entryId: '' + entryPackageName: has-symbols + entryPackageVersion: 1.0.2 + entrySuffix: '' + jsonId: 7 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/has-symbols@1.0.2/node_modules/has-symbols + rawEntryId: /has-symbols/1.0.2 + referrerJsonIds: + - 2 + transitivePeerDependencies: [] +workspace: + pnpmLockfileFolder: '' + pnpmLockfilePath: pnpm-lock.yaml + workspaceRootFullPath: /repo +" +`; diff --git a/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml index da1986e1571..4bf1414f29b 100644 --- a/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml +++ b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml @@ -13,6 +13,14 @@ importers: dependencies: color-string: 2.1.2 + link-specifier/linker: + specifiers: + has-symbols: 1.0.2 + target-folder: link:../target-folder + dependencies: + has-symbols: 1.0.2 + target-folder: link:../target-folder + packages: /color-convert/3.1.2: resolution: @@ -52,3 +60,13 @@ packages: color-convert: 3.1.2 color-string: 2.1.2 dev: false + + /has-symbols/1.0.2: + resolution: + { + integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + } + engines: { node: '>= 0.4' } + dependencies: + target-folder: link:link-specifier/target-folder + dev: false diff --git a/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v6.0.yaml b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v6.0.yaml new file mode 100644 index 00000000000..47d4bd09ffe --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v6.0.yaml @@ -0,0 +1,77 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + duplicate-1/duplicate: + dependencies: + color: + specifier: ^5.0.2 + version: 5.0.2 + + duplicate-2/duplicate: + dependencies: + color-string: + specifier: ^2.1.2 + version: 2.1.2 + + link-specifier/linker: + dependencies: + has-symbols: + specifier: 1.0.2 + version: 1.0.2 + target-folder: + specifier: link:../target-folder + version: link:../target-folder + +packages: + /color-convert@3.1.2: + resolution: + { + integrity: sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg== + } + engines: { node: '>=14.6' } + dependencies: + color-name: 2.0.2 + dev: false + + /color-name@2.0.2: + resolution: + { + integrity: sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A== + } + engines: { node: '>=12.20' } + dev: false + + /color-string@2.1.2: + resolution: + { + integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA== + } + engines: { node: '>=18' } + dependencies: + color-name: 2.0.2 + dev: false + + /color@5.0.2: + resolution: + { + integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA== + } + engines: { node: '>=18' } + dependencies: + color-convert: 3.1.2 + color-string: 2.1.2 + dev: false + + /has-symbols@1.0.2: + resolution: + { + integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + } + engines: { node: '>= 0.4' } + dependencies: + target-folder: link:link-specifier/target-folder + dev: false diff --git a/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v6.0.test.ts b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v6.0.test.ts new file mode 100644 index 00000000000..1a362d47c73 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v6.0.test.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; + +import * as graphTestHelpers from './graphTestHelpers'; + +export const workspace: IJsonLfxWorkspace = { + workspaceRootFullPath: '/repo', + pnpmLockfilePath: 'pnpm-lock.yaml', + pnpmLockfileFolder: '', + rushConfig: undefined +}; + +describe('lfxGraph-edge-cases-v5.4', () => { + it('loads a workspace', async () => { + const serializedYaml: string = await graphTestHelpers.loadAndSerializeLFxGraphAsync({ + lockfilePathUnderFixtures: '/edge-cases/pnpm-lock-v5.4.yaml', + workspace: workspace + }); + expect(serializedYaml).toMatchSnapshot(); + }); +}); From d70134ee5fee2e5a28a47904d924794a1ea3fcfd Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:36:51 -0700 Subject: [PATCH 15/17] PR feedback --- apps/lockfile-explorer/src/graph/lfxGraphLoader.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index 7dfd04e093d..2248b4b1486 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -59,10 +59,7 @@ function createPackageLockfileDependency(options: { result.peerDependencyMeta = { name: result.name, version: version, - optional: - peerDependenciesMeta && peerDependenciesMeta[result.name] - ? peerDependenciesMeta[result.name].optional - : false + optional: peerDependenciesMeta?.[result.name] ? peerDependenciesMeta[result.name].optional : false }; result.entryId = 'Peer: ' + result.name; } else { From 6bebad4f0aa87826bfdecff0876aa6c11c78d4d2 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:50:43 -0700 Subject: [PATCH 16/17] PR feedback: don't try to resolve "link:" under packages section --- .../src/graph/lfxGraphLoader.ts | 65 ++++++++++++++----- .../lfxGraph-edge-cases-v5.4.test.ts.snap | 2 +- .../lfxGraph-edge-cases-v6.0.test.ts.snap | 2 +- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index 2248b4b1486..ba421861c21 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -27,6 +27,7 @@ function createPackageLockfileDependency(options: { containingEntry: LfxGraphEntry; peerDependenciesMeta?: PeerDependenciesMeta; pnpmLockfileVersion: PnpmLockfileVersion; + workspace: IJsonLfxWorkspace; }): LfxGraphDependency { const { name, @@ -48,11 +49,20 @@ function createPackageLockfileDependency(options: { if (version.startsWith('link:')) { const relativePath: string = version.substring('link:'.length); - const rootRelativePath: string = lockfilePath.getAbsolute( - containingEntry.packageJsonFolderPath, - relativePath - ); - result.entryId = 'project:' + rootRelativePath.toString(); + + if (containingEntry.kind === LfxGraphEntryKind.Project) { + // TODO: Here we assume it's a "workspace:" link and try to resolve it to another workspace project, + // but it could also be a link to an arbitrary folder (in which case this entryId will fail to resolve). + // In the future, we should distinguish these cases. + const selfRelativePath: string = lockfilePath.getAbsolute( + containingEntry.packageJsonFolderPath, + relativePath + ); + result.entryId = 'project:' + selfRelativePath.toString(); + } else { + // This could be a link to anywhere on the local computer, so we don't expect it to have a lockfile entry + result.entryId = ''; + } } else if (result.version.startsWith('/')) { result.entryId = version; } else if (result.dependencyType === LfxDependencyKind.Peer) { @@ -79,7 +89,8 @@ function parsePackageDependencies( dependencies: LfxGraphDependency[], lockfileEntry: LfxGraphEntry, either: lockfileTypes.ProjectSnapshot | lockfileTypes.PackageSnapshot, - pnpmLockfileVersion: PnpmLockfileVersion + pnpmLockfileVersion: PnpmLockfileVersion, + workspace: IJsonLfxWorkspace ): void { const node: Partial = either as unknown as Partial; @@ -91,7 +102,8 @@ function parsePackageDependencies( name: packageName, version: version, containingEntry: lockfileEntry, - pnpmLockfileVersion + pnpmLockfileVersion, + workspace }) ); } @@ -104,7 +116,8 @@ function parsePackageDependencies( name: packageName, version: version, containingEntry: lockfileEntry, - pnpmLockfileVersion + pnpmLockfileVersion, + workspace }) ); } @@ -118,7 +131,8 @@ function parsePackageDependencies( version: version, containingEntry: lockfileEntry, peerDependenciesMeta: node.peerDependenciesMeta, - pnpmLockfileVersion + pnpmLockfileVersion, + workspace }) ); } @@ -134,7 +148,8 @@ function parseProjectDependencies60( dependencies: LfxGraphDependency[], lockfileEntry: LfxGraphEntry, snapshot: lockfileTypes.LockfileFileProjectSnapshot, - pnpmLockfileVersion: PnpmLockfileVersion + pnpmLockfileVersion: PnpmLockfileVersion, + workspace: IJsonLfxWorkspace ): void { if (snapshot.dependencies) { for (const [packageName, specifierAndResolution] of Object.entries(snapshot.dependencies)) { @@ -144,7 +159,8 @@ function parseProjectDependencies60( name: packageName, version: specifierAndResolution.version, containingEntry: lockfileEntry, - pnpmLockfileVersion + pnpmLockfileVersion, + workspace }) ); } @@ -157,7 +173,8 @@ function parseProjectDependencies60( name: packageName, version: specifierAndResolution.version, containingEntry: lockfileEntry, - pnpmLockfileVersion + pnpmLockfileVersion, + workspace }) ); } @@ -334,7 +351,13 @@ function createPackageLockfileEntry(options: { ); const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); - parsePackageDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData, pnpmLockfileVersion); + parsePackageDependencies( + lockfileEntry.dependencies, + lockfileEntry, + rawYamlData, + pnpmLockfileVersion, + workspace + ); return lockfileEntry; } @@ -405,13 +428,25 @@ export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfx const lockfile54: lockfileTypes.LockfileObject = lockfileJson as lockfileTypes.LockfileObject; const importerValue: lockfileTypes.ProjectSnapshot = lockfile54.importers[importerKey as pnpmTypes.ProjectId]; - parsePackageDependencies(importer.dependencies, importer, importerValue, pnpmLockfileVersion); + parsePackageDependencies( + importer.dependencies, + importer, + importerValue, + pnpmLockfileVersion, + workspace + ); } else { const lockfile60: lockfileTypes.LockfileFile = lockfileJson as lockfileTypes.LockfileFile; if (lockfile60.importers) { const importerValue: lockfileTypes.LockfileFileProjectSnapshot = lockfile60.importers[importerKey as pnpmTypes.ProjectId]; - parseProjectDependencies60(importer.dependencies, importer, importerValue, pnpmLockfileVersion); + parseProjectDependencies60( + importer.dependencies, + importer, + importerValue, + pnpmLockfileVersion, + workspace + ); } } diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap index 37db52aea0e..382a4991800 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap @@ -141,7 +141,7 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:node_modules/.pnpm/has-symbols@1.0.2/node_modules/has-symbols/link-specifier/target-folder + entryId: '' name: target-folder peerDependencyMeta: {} version: link:link-specifier/target-folder diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap index 37db52aea0e..382a4991800 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap @@ -141,7 +141,7 @@ exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:node_modules/.pnpm/has-symbols@1.0.2/node_modules/has-symbols/link-specifier/target-folder + entryId: '' name: target-folder peerDependencyMeta: {} version: link:link-specifier/target-folder From 9935ac6077f98410883b2d671f84e94e5120bc6f Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:02:48 -0700 Subject: [PATCH 17/17] rush update --- common/config/subspaces/default/repo-state.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index f13a9c6c431..280c8df4e79 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "3749a69d1b0594a63c7a5ad6628f2897dbc3247c", + "pnpmShrinkwrapHash": "260e89de9a23ec7f38ec7956133ae1097057004b", "preferredVersionsHash": "61cd419c533464b580f653eb5f5a7e27fe7055ca" }