UNPKG

@rushstack/lockfile-explorer

Version:

Rush Lockfile Explorer: The UI for solving version conflicts quickly in a large monorepo

495 lines 22.5 kB
"use strict"; // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.generateLockfileGraph = generateLockfileGraph; const node_core_library_1 = require("@rushstack/node-core-library"); const lfx_shared_1 = require("../../build/lfx-shared"); const lockfilePath = __importStar(require("./lockfilePath")); function createPackageLockfileDependency(options) { const { name, versionPath, originalSpecifier, kind: dependencyKind, containingEntry, peerDependenciesMeta, pnpmLockfileVersion } = options; const result = { name, versionPath, entryId: '', originalSpecifier, dependencyKind, peerDependencyMeta: {}, containingEntry }; if (versionPath.startsWith('link:')) { const relativePath = versionPath.substring('link:'.length); if (containingEntry.kind === lfx_shared_1.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 = lockfilePath.getAbsolute(containingEntry.packageJsonFolderPath, relativePath); result.entryId = 'project:' + selfRelativePath.toString(); } else { // This could be a link to anywhere on the local computer, so we don't expect it to have a lockfile entry result.entryId = ''; } } else if (result.versionPath.startsWith('/')) { result.entryId = versionPath; } else { // Version 5.4: /@rushstack/m/1.0.0: // Version 6.0: /@rushstack/m@1.0.0: // Version 9.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) // Version 9.0: @rushstack/j@1.0.0(@rushstack/n@2.0.0) const versionDelimiter = pnpmLockfileVersion < 60 ? '/' : '@'; result.entryId = (pnpmLockfileVersion < 90 ? '/' : '') + result.name + versionDelimiter + result.versionPath; } if (result.dependencyKind === lfx_shared_1.LfxDependencyKind.Peer) { result.peerDependencyMeta = { name: result.name, version: versionPath, optional: (peerDependenciesMeta === null || peerDependenciesMeta === void 0 ? void 0 : peerDependenciesMeta[result.name]) ? peerDependenciesMeta[result.name].optional : false }; } return new lfx_shared_1.LfxGraphDependency(result); } function parsePackageDependencies(options) { const { dependencies, lockfileEntry, mainEntry, specifierEntry, pnpmLockfileVersion, workspace } = options; const node = mainEntry; function createDependency(kind, packageName, versionPath) { let originalSpecifier = undefined; if (specifierEntry && specifierEntry.peerDependencies) { originalSpecifier = specifierEntry.peerDependencies[packageName]; if (originalSpecifier) { kind = lfx_shared_1.LfxDependencyKind.Peer; } } dependencies.push(createPackageLockfileDependency({ kind, name: packageName, versionPath, originalSpecifier: originalSpecifier !== null && originalSpecifier !== void 0 ? originalSpecifier : '', containingEntry: lockfileEntry, peerDependenciesMeta: specifierEntry === null || specifierEntry === void 0 ? void 0 : specifierEntry.peerDependenciesMeta, pnpmLockfileVersion, workspace })); } if (node.dependencies) { for (const [packageName, versionPath] of Object.entries(node.dependencies)) { createDependency(lfx_shared_1.LfxDependencyKind.Regular, packageName, versionPath); } } if (node.optionalDependencies) { for (const [packageName, versionPath] of Object.entries(node.optionalDependencies)) { createDependency(lfx_shared_1.LfxDependencyKind.Regular, packageName, versionPath); } } if (node.devDependencies) { for (const [packageName, versionPath] of Object.entries(node.devDependencies)) { createDependency(lfx_shared_1.LfxDependencyKind.Dev, packageName, versionPath); } } if (node.transitivePeerDependencies) { for (const dep of node.transitivePeerDependencies) { lockfileEntry.transitivePeerDependencies.add(dep); } } } function parseProjectDependencies54(options) { const { dependencies, lockfileEntry, mainEntry, pnpmLockfileVersion, workspace } = options; const node = mainEntry; function createDependency(kind, packageName, versionPath) { let originalSpecifier = undefined; if (mainEntry.specifiers) { originalSpecifier = mainEntry.specifiers[packageName]; } dependencies.push(createPackageLockfileDependency({ kind, name: packageName, versionPath, originalSpecifier: originalSpecifier !== null && originalSpecifier !== void 0 ? originalSpecifier : '', containingEntry: lockfileEntry, pnpmLockfileVersion, workspace })); } if (node.dependencies) { for (const [packageName, versionPath] of Object.entries(node.dependencies)) { createDependency(lfx_shared_1.LfxDependencyKind.Regular, packageName, versionPath); } } if (node.optionalDependencies) { for (const [packageName, versionPath] of Object.entries(node.optionalDependencies)) { createDependency(lfx_shared_1.LfxDependencyKind.Regular, packageName, versionPath); } } if (node.devDependencies) { for (const [packageName, versionPath] of Object.entries(node.devDependencies)) { createDependency(lfx_shared_1.LfxDependencyKind.Dev, packageName, versionPath); } } } function parseProjectDependencies60(dependencies, lockfileEntry, snapshot, pnpmLockfileVersion, workspace) { function createDependency(kind, packageName, specifierAndResolution) { dependencies.push(createPackageLockfileDependency({ kind, name: packageName, versionPath: specifierAndResolution.version, originalSpecifier: specifierAndResolution.specifier, containingEntry: lockfileEntry, pnpmLockfileVersion, workspace })); } if (snapshot.dependencies) { for (const [packageName, specifierAndResolution] of Object.entries(snapshot.dependencies)) { createDependency(lfx_shared_1.LfxDependencyKind.Regular, packageName, specifierAndResolution); } } if (snapshot.optionalDependencies) { for (const [packageName, specifierAndResolution] of Object.entries(snapshot.optionalDependencies)) { createDependency(lfx_shared_1.LfxDependencyKind.Regular, packageName, specifierAndResolution); } } if (snapshot.devDependencies) { for (const [packageName, specifierAndResolution] of Object.entries(snapshot.devDependencies)) { createDependency(lfx_shared_1.LfxDependencyKind.Dev, packageName, specifierAndResolution); } } } function createProjectLockfileEntry(options) { const { rawEntryId, duplicates, workspace } = options; const result = { kind: lfx_shared_1.LfxGraphEntryKind.Project, entryId: '', rawEntryId: '', packageJsonFolderPath: '', entryPackageName: '', displayText: '', entryPackageVersion: '', entrySuffix: '' }; result.rawEntryId = rawEntryId; // Example: pnpmLockfilePath = 'common/temp/my-subspace/pnpm-lock.yaml' // Example: pnpmLockfileFolder = 'common/temp/my-subspace' const pnpmLockfileFolder = workspace.pnpmLockfileFolder; // Example: rawEntryId = '../../../projects/a' // Example: packageJsonFolderPath = 'projects/a' result.packageJsonFolderPath = lockfilePath.getAbsolute(pnpmLockfileFolder, rawEntryId); result.entryId = 'project:' + result.packageJsonFolderPath; const projectFolderName = lockfilePath.getBaseNameOf(rawEntryId); if (!(duplicates === null || duplicates === void 0 ? void 0 : duplicates.has(projectFolderName))) { // TODO: The actual package.json name might not match its directory name, // but we have to load package.json to determine it. result.entryPackageName = projectFolderName; } else { result.entryPackageName = `${projectFolderName} (${result.packageJsonFolderPath})`; } result.displayText = `Project: ${result.entryPackageName}`; const lockfileEntry = new lfx_shared_1.LfxGraphEntry(result); return lockfileEntry; } function createPackageLockfileEntry(options) { const { rawEntryId, pnpmLockfileVersion, workspace } = options; const result = { kind: lfx_shared_1.LfxGraphEntryKind.Package, entryId: rawEntryId, rawEntryId: rawEntryId, packageJsonFolderPath: '', entryPackageName: '', displayText: rawEntryId, entryPackageVersion: '', entrySuffix: '' }; // Example: pnpmLockfilePath = 'common/temp/my-subspace/pnpm-lock.yaml' // Example: pnpmLockfileFolder = 'common/temp/my-subspace' const pnpmLockfileFolder = workspace.pnpmLockfileFolder; let slashlessRawEntryId; if (pnpmLockfileVersion >= 90) { // The leading slash is omitted starting in V9.0 if (rawEntryId.startsWith('/')) { throw new Error('Not expecting leading "/" in path: ' + JSON.stringify(rawEntryId)); } slashlessRawEntryId = rawEntryId; } else { if (!rawEntryId.startsWith('/')) { throw new Error('Expecting leading "/" in path: ' + JSON.stringify(rawEntryId)); } slashlessRawEntryId = rawEntryId.substring(1); } let dotPnpmSubfolder; if (pnpmLockfileVersion < 60) { const lastSlashIndex = rawEntryId.lastIndexOf('/'); if (lastSlashIndex < 0) { throw new Error('Expecting "/" in path: ' + JSON.stringify(rawEntryId)); } const packageName = 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 = rawEntryId.indexOf('_', lastSlashIndex); if (underscoreIndex > 0) { const version = rawEntryId.substring(lastSlashIndex + 1, underscoreIndex); const suffix = 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 = rawEntryId.substring(lastSlashIndex + 1); result.displayText = packageName + ' ' + version; result.entryPackageVersion = version; } // Example: @babel+register@7.17.7_@babel+core@7.17.12 dotPnpmSubfolder = result.entryPackageName.replace('/', '+') + '@' + result.entryPackageVersion + (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; if (slashlessRawEntryId.startsWith('@')) { versionAtSignIndex = slashlessRawEntryId.indexOf('@', 1); } else { versionAtSignIndex = slashlessRawEntryId.indexOf('@'); } const packageName = slashlessRawEntryId.substring(0, versionAtSignIndex); result.entryPackageName = packageName; const leftParenIndex = slashlessRawEntryId.indexOf('(', versionAtSignIndex); if (leftParenIndex < 0) { const version = slashlessRawEntryId.substring(versionAtSignIndex + 1); result.entryPackageVersion = version; // @rushstack/eslint-config@3.0.1 // --> @rushstack/eslint-config 3.0.1 result.displayText = packageName + ' ' + version; } else { const version = slashlessRawEntryId.substring(versionAtSignIndex + 1, leftParenIndex); result.entryPackageVersion = version; // "(@rushstack/m@1.0.0)(@rushstack/n@2.0.0)" let suffix = slashlessRawEntryId.substring(leftParenIndex); // Rewrite to: // "@rushstack/m@1.0.0; @rushstack/n@2.0.0" suffix = node_core_library_1.Text.replaceAll(suffix, ')(', '; '); suffix = node_core_library_1.Text.replaceAll(suffix, '(', ''); suffix = node_core_library_1.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 + ']'; } // 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 = node_core_library_1.Text.replaceAll(slashlessRawEntryId, '/', '+'); dotPnpmSubfolder = node_core_library_1.Text.replaceAll(dotPnpmSubfolder, ')(', '_'); dotPnpmSubfolder = node_core_library_1.Text.replaceAll(dotPnpmSubfolder, '(', '_'); dotPnpmSubfolder = node_core_library_1.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 = new lfx_shared_1.LfxGraphEntry(result); return lockfileEntry; } /** * Parse through the lockfile and create all the corresponding LockfileEntries and LockfileDependencies * to construct the lockfile graph. * * @returns A list of all the LockfileEntries in the lockfile. */ function generateLockfileGraph(lockfileJson, workspace) { const lockfile = lockfileJson; let 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 = new lfx_shared_1.LfxGraph(workspace); const allEntries = lfxGraph.entries; const allEntriesById = new Map(); const allImporters = []; // "Importers" are the local workspace projects if (lockfile.importers) { // 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 = new Set(); const duplicates = new Set(); for (const importerKey of Object.keys(lockfile.importers)) { const baseName = lockfilePath.getBaseNameOf(importerKey); if (baseNames.has(baseName)) { duplicates.add(baseName); } baseNames.add(baseName); } const isRushWorkspace = workspace.rushConfig !== undefined; for (const importerKey of Object.keys(lockfile.importers)) { if (isRushWorkspace && importerKey === '.') { // Discard the synthetic package.json file created by Rush under common/temp continue; } const importer = createProjectLockfileEntry({ rawEntryId: importerKey, duplicates, workspace, pnpmLockfileVersion }); if (pnpmLockfileVersion < 60) { const lockfile54 = lockfileJson; const importerValue = lockfile54.importers[importerKey]; parseProjectDependencies54({ dependencies: importer.dependencies, lockfileEntry: importer, mainEntry: importerValue, pnpmLockfileVersion, workspace }); } else { const lockfile60 = lockfileJson; if (lockfile60.importers) { const importerValue = lockfile60.importers[importerKey]; parseProjectDependencies60(importer.dependencies, importer, importerValue, pnpmLockfileVersion, workspace); } } allImporters.push(importer); allEntries.push(importer); allEntriesById.set(importer.entryId, importer); } } if (pnpmLockfileVersion < 90) { if (lockfile.packages) { for (const [dependencyKey, dependencyValue] of Object.entries(lockfile.packages)) { const lockfileEntry = createPackageLockfileEntry({ rawEntryId: dependencyKey, workspace, pnpmLockfileVersion }); parsePackageDependencies({ dependencies: lockfileEntry.dependencies, lockfileEntry: lockfileEntry, mainEntry: dependencyValue, specifierEntry: dependencyValue, pnpmLockfileVersion, workspace }); allEntries.push(lockfileEntry); allEntriesById.set(dependencyKey, lockfileEntry); } } } else { const packagesByKey = new Map(); if (lockfile.packages) { for (const [dependencyKey, dependencyValue] of Object.entries(lockfile.packages)) { packagesByKey.set(dependencyKey, dependencyValue); } } // In v9.0 format, the dependency graph for non-workspace packages is found under "snapshots" not "packages". // (The "packages" section now stores other fields that are unrelated to the graph itself.) const lockfile90 = lockfileJson; if (lockfile90.snapshots) { for (const [dependencyKey, dependencyValue] of Object.entries(lockfile90.snapshots)) { const lockfileEntry = createPackageLockfileEntry({ rawEntryId: dependencyKey, workspace, pnpmLockfileVersion }); // Example: "@scope/my-package@1.0.0" const packageInfoKey = lockfileEntry.entryPackageName + '@' + lockfileEntry.entryPackageVersion; const packageInfo = packagesByKey.get(packageInfoKey); parsePackageDependencies({ dependencies: lockfileEntry.dependencies, lockfileEntry, mainEntry: dependencyValue, specifierEntry: packageInfo, pnpmLockfileVersion, workspace }); allEntries.push(lockfileEntry); allEntriesById.set(lockfileEntry.entryId, lockfileEntry); } } } // Construct the graph for (const entry of allEntries) { for (const dependency of entry.dependencies) { // Peer dependencies do not have a matching entry if (dependency.dependencyKind === lfx_shared_1.LfxDependencyKind.Peer) { continue; } const matchedEntry = allEntriesById.get(dependency.entryId); if (matchedEntry) { // Create a two-way link between the dependency and the entry dependency.resolvedEntry = matchedEntry; matchedEntry.referrers.push(entry); } else { if (dependency.entryId.startsWith('/')) { // Local package console.error('Could not resolve dependency entryId: ', dependency.entryId, dependency); } } } } return lfxGraph; } //# sourceMappingURL=lfxGraphLoader.js.map