@rushstack/lockfile-explorer
Version:
Rush Lockfile Explorer: The UI for solving version conflicts quickly in a large monorepo
495 lines • 22.5 kB
JavaScript
"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