Skip to content

Commit b14a819

Browse files
authored
[lockfile-explorer] Rewrite lockfile parser to be more correct (#5363)
* Add edge case for projects with duplicate names * Replace custom interfaces with official PNPM types; temporarily remove the V6 kludge * Add lockfilePath.ts utility that can completely eliminate `@lifaon/path` * Completed rewrite of 5.4 loader logic * Remove "@lifaon/path" dependency * Splite createLockfileEntry() into createProjectLockfileEntry() and createPackageLockfileEntry() * Upgrade to use @pnpm/lockfile.types@1002.0.1 * Implement support for lockfile version 6.0 * Hide the "." package from Rush workspaces, shuffling the jsonId's in all the snapshots * Fix an incorrect path * rush change * Improve formatting of 6.0 suffixes * Move entry to nonbrowser-approved-packages.json * Add a failing test case for "link:" in packages section * PR feedback * PR feedback: don't try to resolve "link:" under packages section * rush update
1 parent e8d82f4 commit b14a819

29 files changed

Lines changed: 1344 additions & 508 deletions

apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
export interface IJsonLfxWorkspaceRushConfig {
55
/**
6-
* The rushVersion from rush.json.
6+
* The `rushVersion` field from rush.json.
77
*/
88
readonly rushVersion: string;
99

@@ -16,19 +16,36 @@ export interface IJsonLfxWorkspaceRushConfig {
1616

1717
export interface IJsonLfxWorkspace {
1818
/**
19-
* Absolute path to the workspace folder that is opened by the app.
20-
* Relative paths are generally relative to this path.
19+
* Absolute path to the workspace folder that is opened by the app, normalized to use forward slashes
20+
* without a trailing slash.
21+
*
22+
* @example `"C:/path/to/MyRepo"`
2123
*/
22-
readonly workspaceRootFolder: string;
24+
readonly workspaceRootFullPath: string;
2325

2426
/**
25-
* The path to the pnpm-lock.yaml file.
27+
* The path to the "pnpm-lock.yaml" file, relative to `workspaceRootFullPath`
28+
* and normalized to use forward slashes without a leading slash.
29+
*
30+
* @example `"common/temp/my-subspace/pnpm-lock.yaml"`
31+
* @example `"pnpm-lock.yaml"`
2632
*/
2733
readonly pnpmLockfilePath: string;
2834

2935
/**
30-
* If this is a Rush workspace (versus a plain PNPM workspace), then
31-
* this section will be defined.
36+
* The path to the folder of "pnpm-lock.yaml" file, relative to `workspaceRootFullPath`
37+
* and normalized to use forward slashes without a leading slash.
38+
*
39+
* If `pnpm-lack.yaml` is in the `workspaceRootFullPath` folder, then pnpmLockfileFolder
40+
* is the empty string.
41+
*
42+
* @example `"common/temp/my-subspace"`
43+
* @example `""`
44+
*/
45+
readonly pnpmLockfileFolder: string;
46+
47+
/**
48+
* This section will be defined only if this is a Rush workspace (versus a plain PNPM workspace).
3249
*/
3350
readonly rushConfig: IJsonLfxWorkspaceRushConfig | undefined;
3451
}

apps/lockfile-explorer/.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"name": "Single Jest test",
2121
"program": "${workspaceFolder}/node_modules/@rushstack/heft/lib/start.js",
2222
"cwd": "${workspaceFolder}",
23-
"args": ["--debug", "test", "--clean", "-u", "--test-path-pattern", "lfxGraphLoader60"],
23+
"args": ["--debug", "test", "--clean", "-u", "--test-path-pattern", "lfxGraph-website-sample-1-v6.0.test"],
2424
"console": "integratedTerminal",
2525
"sourceMaps": true
2626
},

apps/lockfile-explorer/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"_phase:test": "heft run --only test -- --clean"
4040
},
4141
"peerDependencies": {
42-
"@types/express": "^4.17.21"
42+
"@types/express": "^5.0.3"
4343
},
4444
"peerDependenciesMeta": {
4545
"@types/express": {
@@ -55,12 +55,12 @@
5555
"@types/update-notifier": "~6.0.1",
5656
"eslint": "~9.25.1",
5757
"local-node-rig": "workspace:*",
58-
"@pnpm/lockfile-types": "^5.1.5",
58+
"@pnpm/lockfile.types": "1002.0.1",
59+
"@pnpm/types": "1000.8.0",
5960
"@types/semver": "7.5.0"
6061
},
6162
"dependencies": {
6263
"tslib": "~2.8.1",
63-
"@lifaon/path": "~2.1.0",
6464
"@microsoft/rush-lib": "workspace:*",
6565
"@pnpm/dependency-path-lockfile-pre-v9": "npm:@pnpm/dependency-path@~2.1.2",
6666
"@rushstack/node-core-library": "workspace:*",

apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
CommandLineParser,
1616
type IRequiredCommandLineStringParameter
1717
} from '@rushstack/ts-command-line';
18-
import type { Lockfile } from '@pnpm/lockfile-types';
1918
import {
2019
type LfxGraph,
2120
lfxGraphSerializer,
@@ -152,13 +151,9 @@ export class ExplorerCommandLineParser extends CommandLineParser {
152151

153152
app.get('/api/graph', async (req: express.Request, res: express.Response) => {
154153
const pnpmLockfileText: string = await FileSystem.readFileAsync(appState.pnpmLockfileLocation);
155-
const lockfile: Lockfile = yaml.load(pnpmLockfileText) as Lockfile;
154+
const lockfile: unknown = yaml.load(pnpmLockfileText) as unknown;
156155

157-
const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(
158-
appState.lfxWorkspace,
159-
lockfile as lfxGraphLoader.ILockfilePackageType,
160-
appState.lfxWorkspace.rushConfig?.subspaceName ?? ''
161-
);
156+
const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(lockfile, appState.lfxWorkspace);
162157

163158
const jsonGraph: IJsonLfxGraph = lfxGraphSerializer.serializeToJson(graph);
164159
res.type('application/json').send(jsonGraph);

apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { RushConfiguration, type RushConfigurationProject, type Subspace } from
77
import path from 'path';
88
import yaml from 'js-yaml';
99
import semver from 'semver';
10+
import type * as lockfileTypes from '@pnpm/lockfile.types';
11+
import type * as pnpmTypes from '@pnpm/types';
1012
import { AlreadyReportedError, Async, FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library';
1113

1214
import lockfileLintSchema from '../../../schemas/lockfile-lint.schema.json';
@@ -17,7 +19,6 @@ import {
1719
parseDependencyPath,
1820
splicePackageWithVersion
1921
} from '../../../utils/shrinkwrap';
20-
import type { Lockfile, LockfileV6 } from '@pnpm/lockfile-types';
2122

2223
export interface ILintRule {
2324
rule: 'restrict-versions';
@@ -40,7 +41,7 @@ export class CheckAction extends CommandLineAction {
4041

4142
private _rushConfiguration!: RushConfiguration;
4243
private _checkedProjects: Set<RushConfigurationProject>;
43-
private _docMap: Map<string, Lockfile | LockfileV6>;
44+
private _docMap: Map<string, lockfileTypes.LockfileObject>;
4445

4546
public constructor(parser: LintCommandLineParser) {
4647
super({
@@ -59,8 +60,8 @@ export class CheckAction extends CommandLineAction {
5960

6061
private async _checkVersionCompatibilityAsync(
6162
shrinkwrapFileMajorVersion: number,
62-
packages: Lockfile['packages'],
63-
dependencyPath: string,
63+
packages: lockfileTypes.PackageSnapshots | undefined,
64+
dependencyPath: pnpmTypes.DepPath,
6465
requiredVersions: Record<string, string>,
6566
checkedDependencyPaths: Set<string>
6667
): Promise<void> {
@@ -84,7 +85,7 @@ export class CheckAction extends CommandLineAction {
8485
shrinkwrapFileMajorVersion,
8586
dependencyPackageName,
8687
dependencyPackageVersion
87-
),
88+
) as pnpmTypes.DepPath,
8889
requiredVersions,
8990
checkedDependencyPaths
9091
);
@@ -103,12 +104,12 @@ export class CheckAction extends CommandLineAction {
103104
const projectFolder: string = project.projectFolder;
104105
const subspace: Subspace = project.subspace;
105106
const shrinkwrapFilename: string = subspace.getCommittedShrinkwrapFilePath();
106-
let doc: Lockfile | LockfileV6;
107+
let doc: lockfileTypes.LockfileObject;
107108
if (this._docMap.has(shrinkwrapFilename)) {
108109
doc = this._docMap.get(shrinkwrapFilename)!;
109110
} else {
110111
const pnpmLockfileText: string = await FileSystem.readFileAsync(shrinkwrapFilename);
111-
doc = yaml.load(pnpmLockfileText) as Lockfile | LockfileV6;
112+
doc = yaml.load(pnpmLockfileText) as lockfileTypes.LockfileObject;
112113
this._docMap.set(shrinkwrapFilename, doc);
113114
}
114115
const { importers, lockfileVersion, packages } = doc;
@@ -120,7 +121,7 @@ export class CheckAction extends CommandLineAction {
120121
if (path.resolve(projectFolder, relativePath) === projectFolder) {
121122
const dependenciesEntries: [string, unknown][] = Object.entries(dependencies ?? {});
122123
for (const [dependencyName, dependencyValue] of dependenciesEntries) {
123-
const fullDependencyPath: string = splicePackageWithVersion(
124+
const fullDependencyPath: pnpmTypes.DepPath = splicePackageWithVersion(
124125
shrinkwrapFileMajorVersion,
125126
dependencyName,
126127
typeof dependencyValue === 'string'
@@ -131,7 +132,7 @@ export class CheckAction extends CommandLineAction {
131132
specifier: string;
132133
}
133134
).version
134-
);
135+
) as pnpmTypes.DepPath;
135136
if (fullDependencyPath.includes('link:')) {
136137
const dependencyProject: RushConfigurationProject | undefined =
137138
this._rushConfiguration.getProjectByName(dependencyName);

0 commit comments

Comments
 (0)