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/.vscode/launch.json b/apps/lockfile-explorer/.vscode/launch.json index 9cc2fd9f58d..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", "lfxGraphLoader60"], + "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/package.json b/apps/lockfile-explorer/package.json index 4e175cb7851..62e03b0be7f 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,12 +55,12 @@ "@types/update-notifier": "~6.0.1", "eslint": "~9.25.1", "local-node-rig": "workspace:*", - "@pnpm/lockfile-types": "^5.1.5", + "@pnpm/lockfile.types": "1002.0.1", + "@pnpm/types": "1000.8.0", "@types/semver": "7.5.0" }, "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/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index cc40653e354..cf0bd6e7839 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,13 +151,9 @@ 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, - 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/cli/lint/actions/CheckAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts index 389bb27001b..12390a1b274 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.LockfileObject; 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.LockfileObject; 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..ba421861c21 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -1,7 +1,9 @@ // 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'; +import { Text } from '@rushstack/node-core-library'; import { type ILfxGraphDependencyOptions, @@ -13,77 +15,29 @@ import { LfxGraphDependency, type IJsonLfxWorkspace } from '../../build/lfx-shared'; +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; + workspace: IJsonLfxWorkspace; +}): LfxGraphDependency { + const { + name, + version, + kind: dependencyType, + containingEntry, + peerDependenciesMeta, + pnpmLockfileVersion + } = options; -import { convertLockfileV6DepPathToV5DepPath } from '../utils/shrinkwrap'; - -enum PnpmLockfileVersion { - V6, - 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( - name: string, - version: string, - dependencyType: LfxDependencyKind, - containingEntry: LfxGraphEntry, - node?: ILockfileNode -): LfxGraphDependency { const result: ILfxGraphDependencyOptions = { name, version, @@ -95,58 +49,91 @@ 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) - ); - if (!rootRelativePath) { - console.error('No root relative path for dependency!', name); - return new LfxGraphDependency(result); + + 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 = ''; } - result.entryId = 'project:' + rootRelativePath.toString(); } 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?.[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, - node: ILockfileNode + either: lockfileTypes.ProjectSnapshot | lockfileTypes.PackageSnapshot, + pnpmLockfileVersion: PnpmLockfileVersion, + workspace: IJsonLfxWorkspace ): 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( - createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Regular, lockfileEntry) + createPackageLockfileDependency({ + kind: LfxDependencyKind.Regular, + name: packageName, + version: version, + containingEntry: lockfileEntry, + pnpmLockfileVersion, + workspace + }) ); } } if (node.devDependencies) { - for (const [pkgName, pkgVersion] of Object.entries(node.devDependencies)) { - dependencies.push(createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Dev, lockfileEntry)); + for (const [packageName, version] of Object.entries(node.devDependencies)) { + dependencies.push( + createPackageLockfileDependency({ + kind: LfxDependencyKind.Dev, + name: packageName, + version: version, + containingEntry: lockfileEntry, + pnpmLockfileVersion, + workspace + }) + ); } } if (node.peerDependencies) { - for (const [pkgName, pkgVersion] of Object.entries(node.peerDependencies)) { + for (const [packageName, version] of Object.entries(node.peerDependencies)) { dependencies.push( - createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Peer, lockfileEntry, node) + createPackageLockfileDependency({ + kind: LfxDependencyKind.Peer, + name: packageName, + version: version, + containingEntry: lockfileEntry, + peerDependenciesMeta: node.peerDependenciesMeta, + pnpmLockfileVersion, + workspace + }) ); } } @@ -157,17 +144,53 @@ function parseDependencies( } } -function createLockfileEntry(options: { +function parseProjectDependencies60( + dependencies: LfxGraphDependency[], + lockfileEntry: LfxGraphEntry, + snapshot: lockfileTypes.LockfileFileProjectSnapshot, + pnpmLockfileVersion: PnpmLockfileVersion, + workspace: IJsonLfxWorkspace +): 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, + workspace + }) + ); + } + } + 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, + workspace + }) + ); + } + } +} + +function createProjectLockfileEntry(options: { rawEntryId: string; - kind: LfxGraphEntryKind; - rawYamlData: ILockfileNode; duplicates?: Set; - subspaceName?: string; + workspace: IJsonLfxWorkspace; + pnpmLockfileVersion: PnpmLockfileVersion; }): LfxGraphEntry { - const { rawEntryId, kind, rawYamlData, duplicates, subspaceName } = options; + const { rawEntryId, duplicates, workspace } = options; const result: ILfxGraphEntryOptions = { - kind, + kind: LfxGraphEntryKind.Project, entryId: '', rawEntryId: '', packageJsonFolderPath: '', @@ -179,111 +202,163 @@ 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(); - - if (!packageJsonFolderPath || !packageName) { - console.error('Could not construct path for entry: ', rawEntryId); - return new LfxGraphEntry(result); - } + // Example: rawEntryId = '../../../projects/a' + // Example: packageJsonFolderPath = 'projects/a' + result.packageJsonFolderPath = lockfilePath.getAbsolute(pnpmLockfileFolder, rawEntryId); + result.entryId = 'project:' + result.packageJsonFolderPath; - 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})`; - } else { - result.displayText = 'Project: ' + result.entryPackageName; - } + 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.displayText = rawEntryId; + result.entryPackageName = `${projectFolderName} (${result.packageJsonFolderPath})`; + } + result.displayText = `Project: ${result.entryPackageName}`; + + const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); + return lockfileEntry; +} - const match: RegExpExecArray | null = packageEntryIdRegex.exec(rawEntryId); +function createPackageLockfileEntry(options: { + rawEntryId: string; + rawYamlData: lockfileTypes.PackageSnapshot; + workspace: IJsonLfxWorkspace; + pnpmLockfileVersion: PnpmLockfileVersion; +}): LfxGraphEntry { + const { rawEntryId, rawYamlData, pnpmLockfileVersion, workspace } = options; - if (match) { - const [, packageName, versionPart] = match; - result.entryPackageName = packageName; + const result: ILfxGraphEntryOptions = { + kind: LfxGraphEntryKind.Package, + entryId: '', + rawEntryId: '', + packageJsonFolderPath: '', + entryPackageName: '', + displayText: '', + entryPackageVersion: '', + entrySuffix: '' + }; - const underscoreIndex: number = versionPart.indexOf('_'); - if (underscoreIndex >= 0) { - const version: string = versionPart.substring(0, underscoreIndex); - const suffix: string = versionPart.substring(underscoreIndex + 1); + result.rawEntryId = rawEntryId; - result.entryPackageVersion = version; - result.entrySuffix = suffix; + // Example: pnpmLockfilePath = 'common/temp/my-subspace/pnpm-lock.yaml' + // Example: pnpmLockfileFolder = 'common/temp/my-subspace' + const pnpmLockfileFolder: string = workspace.pnpmLockfileFolder; - // /@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; + result.displayText = rawEntryId; - // /@rushstack/eslint-config/3.0.1 - // --> @rushstack/eslint-config 3.0.1 - result.displayText = packageName + ' ' + versionPart; - } + if (!rawEntryId.startsWith('/')) { + throw new Error('Expecting leading "/" in path: ' + JSON.stringify(rawEntryId)); + } + + let dotPnpmSubfolder: string; + + if (pnpmLockfileVersion === 54) { + 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: - // 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/` + + // Example: @babel+register@7.17.7_@babel+core@7.17.12 + dotPnpmSubfolder = result.entryPackageName.replace('/', '+') + '@' + result.entryPackageVersion + - (result.entrySuffix ? `_${result.entrySuffix}` : '') + - '/node_modules/' + - result.entryPackageName; - } + (result.entrySuffix ? `_${result.entrySuffix}` : ''); + } else { + // Example inputs: + // /@rushstack/eslint-config@3.0.1 + // /@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 lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); - parseDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData); - return lockfileEntry; -} + const leftParenIndex: number = rawEntryId.indexOf('(', versionAtSignIndex); + if (leftParenIndex < 0) { + const version: string = rawEntryId.substring(versionAtSignIndex + 1); + result.entryPackageVersion = version; -/** - * 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; + // /@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; + + // "(@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) + // --> @rushstack/l 1.0.0 [@rushstack/m@1.0.0; @rushstack/n@2.0.0] + result.displayText = packageName + ' ' + version + ' [' + suffix + ']'; } - return v5ImporterValue; - } else { - return importerValue as ILockfileImporterV5; + + // 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 + // /@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); + parsePackageDependencies( + lockfileEntry.dependencies, + lockfileEntry, + rawYamlData, + pnpmLockfileVersion, + workspace + ); + return lockfileEntry; } /** @@ -292,77 +367,110 @@ function getImporterValue( * * @returns A list of all the LockfileEntries in the lockfile. */ -export function generateLockfileGraph( - workspace: IJsonLfxWorkspace, - lockfile: ILockfilePackageType, - subspaceName?: string -): LfxGraph { - 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'] = {}; - for (const [dependencyPath, dependency] of Object.entries(lockfile.packages)) { - updatedPackages[convertLockfileV6DepPathToV5DepPath(dependencyPath)] = dependency; - } - lockfile.packages = updatedPackages; +export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfxWorkspace): LfxGraph { + const lockfile: lockfileTypes.LockfileObject | lockfileTypes.LockfileFile = lockfileJson as + | lockfileTypes.LockfileObject + | lockfileTypes.LockfileFile; + + 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; - // const normalizedPath = new Path(importerKey).makeAbsolute('/').toString(); - const importer: LfxGraphEntry = createLockfileEntry({ - // entryId: normalizedPath, + 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 importer: LfxGraphEntry = createProjectLockfileEntry({ rawEntryId: importerKey, - kind: LfxGraphEntryKind.Project, - rawYamlData: getImporterValue(importerValue, pnpmLockfileVersion), duplicates, - subspaceName + 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, + 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, + workspace + ); + } + } + allImporters.push(importer); allEntries.push(importer); - allEntriesById[importer.entryId] = importer; + allEntriesById.set(importer.entryId, importer); } } 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, + const currEntry: LfxGraphEntry = createPackageLockfileEntry({ rawEntryId: dependencyKey, - kind: LfxGraphEntryKind.Package, - rawYamlData: dependencyValue, - subspaceName + rawYamlData: dependencyValue as lockfileTypes.PackageSnapshot, + workspace, + pnpmLockfileVersion }); allPackages.push(currEntry); allEntries.push(currEntry); - allEntriesById[dependencyKey] = currEntry; + allEntriesById.set(dependencyKey, currEntry); } } @@ -374,7 +482,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/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/__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..382a4991800 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.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: '' + 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/__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..382a4991800 --- /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: '' + 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/__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..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,166 +2,154 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` "entries: - - dependencies: [] - displayText: '' - entryId: '' - entryPackageName: '' - entryPackageVersion: '' - entrySuffix: '' - jsonId: 0 - kind: 1 - packageJsonFolderPath: '' - rawEntryId: . - referrerJsonIds: [] - transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d displayText: 'Project: a' - entryId: project:./common/projects/a + entryId: project:projects/a entryPackageName: a entryPackageVersion: '' entrySuffix: '' - jsonId: 1 + jsonId: 0 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 + 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:./common/projects/b + entryId: project:projects/b entryPackageName: b entryPackageVersion: '' entrySuffix: '' - jsonId: 2 + jsonId: 1 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 + 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:./common/projects/c + entryId: project:projects/c entryPackageName: c entryPackageVersion: '' entrySuffix: '' - jsonId: 3 + jsonId: 2 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 + 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:./common/projects/d + entryId: project:projects/d entryPackageName: d entryPackageVersion: '' entrySuffix: '' - jsonId: 4 + jsonId: 3 kind: 1 - packageJsonFolderPath: ./common/projects/d + 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:./common/projects/e + entryId: project:projects/e entryPackageName: e entryPackageVersion: '' entrySuffix: '' - jsonId: 5 + jsonId: 4 kind: 1 - packageJsonFolderPath: ./common/projects/e + 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/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 + - 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/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 + - 2 transitivePeerDependencies: - '@rushstack/m' - '@rushstack/n' @@ -190,20 +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/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 + - 5 transitivePeerDependencies: - '@rushstack/m' - '@rushstack/n' @@ -212,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' @@ -235,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/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 + - 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' @@ -276,13 +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/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 + - 7 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/m 1.0.0' @@ -290,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/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 - - 6 + - 2 + - 5 + - 8 - 9 - - 10 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 2.0.0' @@ -306,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/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 - - 4 - - 10 + - 1 + - 3 + - 9 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 3.0.0' @@ -321,18 +307,19 @@ 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/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 + - 4 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/__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..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 @@ -2,204 +2,191 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` "entries: - - dependencies: [] - displayText: '' - entryId: '' - entryPackageName: '' - entryPackageVersion: '' - entrySuffix: '' - jsonId: 0 - kind: 1 - packageJsonFolderPath: '' - rawEntryId: . - referrerJsonIds: [] - transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d displayText: 'Project: a' - entryId: project:./common/projects/a + entryId: project:projects/a entryPackageName: a entryPackageVersion: '' entrySuffix: '' - jsonId: 1 + jsonId: 0 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 + resolvedEntryJsonId: 3 version: link:../d - dependencyType: regular - entryId: /@rushstack/n/2.0.0 + entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 2.0.0 displayText: 'Project: b' - entryId: project:./common/projects/b + entryId: project:projects/b entryPackageName: b entryPackageVersion: '' entrySuffix: '' - jsonId: 2 + jsonId: 1 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 + resolvedEntryJsonId: 4 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 + resolvedEntryJsonId: 6 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 + resolvedEntryJsonId: 8 version: 1.0.0 displayText: 'Project: c' - entryId: project:./common/projects/c + entryId: project:projects/c entryPackageName: c entryPackageVersion: '' entrySuffix: '' - jsonId: 3 + jsonId: 2 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 + resolvedEntryJsonId: 4 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 + resolvedEntryJsonId: 5 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 + resolvedEntryJsonId: 9 version: 2.0.0 displayText: 'Project: d' - entryId: project:./common/projects/d + entryId: project:projects/d entryPackageName: d entryPackageVersion: '' entrySuffix: '' - jsonId: 4 + jsonId: 3 kind: 1 - packageJsonFolderPath: ./common/projects/d + packageJsonFolderPath: projects/d rawEntryId: ../../projects/d referrerJsonIds: + - 0 - 1 - - 2 transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: /@rushstack/n/3.0.0 + entryId: /@rushstack/n@3.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 3.0.0 displayText: 'Project: e' - entryId: project:./common/projects/e + entryId: project:projects/e entryPackageName: e entryPackageVersion: '' entrySuffix: '' - jsonId: 5 + jsonId: 4 kind: 1 - packageJsonFolderPath: ./common/projects/e + 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) + 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 + 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)' + 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: '' - jsonId: 6 + entryPackageName: '@rushstack/j' + entryPackageVersion: 1.0.0 + entrySuffix: '@rushstack/n@2.0.0' + jsonId: 5 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 + - 3 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 + 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/1.0.0(@rushstack/m@1.0.0)(@rushstack' - entryPackageVersion: n@2.0.0) - entrySuffix: '' - jsonId: 7 + entryPackageName: '@rushstack/k' + entryPackageVersion: 1.0.0 + entrySuffix: '@rushstack/m@1.0.0; @rushstack/n@2.0.0' + jsonId: 6 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 + - 2 + - 5 transitivePeerDependencies: - '@rushstack/m' - '@rushstack/n' - dependencies: - dependencyType: regular - entryId: /@rushstack/m/1.0.0 + 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 + entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 2.0.0 - dependencyType: peer entryId: 'Peer: @rushstack/m' @@ -217,18 +204,18 @@ 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: '' - jsonId: 8 + entryPackageName: '@rushstack/l' + 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+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 + - 6 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/m 1.0.0' @@ -236,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/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 - - 8 + - 2 + - 5 + - 7 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 2.0.0' @@ -251,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/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 - - 8 + - 1 + - 3 + - 7 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 3.0.0' @@ -266,18 +253,19 @@ 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/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 + - 4 transitivePeerDependencies: [] workspace: + pnpmLockfileFolder: common/temp pnpmLockfilePath: common/temp/pnpm-lock.yaml rushConfig: rushVersion: 5.158.1 subspaceName: '' - workspaceRootFolder: /repo + 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 new file mode 100644 index 00000000000..4bf1414f29b --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml @@ -0,0 +1,72 @@ +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 + + 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: + { + 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/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/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/graphTestHelpers.ts b/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts index 52583dec6c2..74d99eb35de 100644 --- a/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts +++ b/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts @@ -23,8 +23,8 @@ export async function loadAndSerializeLFxGraphAsync(options: { FIXTURES_FOLDER + options.lockfilePathUnderFixtures, { convertLineEndings: NewlineKind.Lf } ); - const lockfileObject = yaml.load(lockfileYaml) as lfxGraphLoader.ILockfilePackageType; - const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(options.workspace, lockfileObject); + const lockfileObject = yaml.load(lockfileYaml); + 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 new file mode 100644 index 00000000000..1a362d47c73 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.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(); + }); +}); 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(); + }); +}); 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/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/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/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'); + }); +}); diff --git a/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts b/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts index 977cae423be..46ffc2c09db 100644 --- a/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts +++ b/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts @@ -8,25 +8,11 @@ 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": "", - "entryPackageVersion": "", - "entrySuffix": "", - "jsonId": 0, - "kind": 1, - "packageJsonFolderPath": "", - "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,18 +36,18 @@ Object { "optional": undefined, "version": undefined, }, - "resolvedEntryJsonId": 3, + "resolvedEntryJsonId": 2, "version": "1.7.1", }, ], "displayText": "Project: testApp1", - "entryId": "project:./apps/testApp1", + "entryId": "project:apps/testApp1", "entryPackageName": "testApp1", "entryPackageVersion": "", "entrySuffix": "", - "jsonId": 1, + "jsonId": 0, "kind": 1, - "packageJsonFolderPath": "./apps/testApp1", + "packageJsonFolderPath": "apps/testApp1", "rawEntryId": "../../../apps/testApp1", "referrerJsonIds": Array [], "transitivePeerDependencies": Array [], @@ -73,12 +59,12 @@ Object { "entryPackageName": "@testPackage/core", "entryPackageVersion": "1.7.1", "entrySuffix": "", - "jsonId": 2, + "jsonId": 1, "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, + 0, ], "transitivePeerDependencies": Array [], }, @@ -89,27 +75,31 @@ Object { "entryPackageName": "@testPackage2/core", "entryPackageVersion": "1.7.1", "entrySuffix": "", - "jsonId": 3, + "jsonId": 2, "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, + 0, ], "transitivePeerDependencies": Array [], }, ], "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..55f21779109 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.dirname(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 } }; 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 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" ] 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 3818f460807..a7daffa4a97 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 @@ -236,9 +233,12 @@ importers: specifier: ~5.1.0 version: 5.1.0 devDependencies: - '@pnpm/lockfile-types': - specifier: ^5.1.5 - version: 5.1.5 + '@pnpm/lockfile.types': + specifier: 1002.0.1 + version: 1002.0.1 + '@pnpm/types': + specifier: 1000.8.0 + version: 1000.8.0 '@rushstack/heft': specifier: workspace:* version: link:../heft @@ -10165,10 +10165,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: @@ -10485,13 +10481,6 @@ 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'} - dependencies: - '@pnpm/types': 9.4.2 - dev: true - /@pnpm/lockfile.types@1.0.3: resolution: {integrity: sha512-A7vUWktnhDkrIs+WmXm7AdffJVyVYJpQUEouya/DYhB+Y+tQ3BXjZ6CV0KybqLgI/8AZErgCJqFxA0GJH6QDjA==} engines: {node: '>=18.12'} @@ -10499,6 +10488,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'} @@ -10520,6 +10518,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'} @@ -10555,11 +10558,23 @@ 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'} dev: false + /@pnpm/types@1000.8.0: + resolution: {integrity: sha512-yx86CGHHquWAI0GgKIuV/RnYewcf5fVFZemC45C/K2cX0uV8GB8TUP541ZrokWola2fZx5sn1vL7xzbceRZfoQ==} + engines: {node: '>=18.12'} + dev: true + /@pnpm/types@12.2.0: resolution: {integrity: sha512-5RtwWhX39j89/Tmyv2QSlpiNjErA357T/8r1Dkg+2lD3P7RuS7Xi2tChvmOC3VlezEFNcWnEGCOeKoGRkDuqFA==} engines: {node: '>=18.12'} @@ -10577,6 +10592,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 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" }