@rushstack/lockfile-explorer
Version:
Rush Lockfile Explorer: The UI for solving version conflicts quickly in a large monorepo
138 lines • 7.63 kB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import path from 'node:path';
import yaml from 'js-yaml';
import semver from 'semver';
import { RushConfiguration } from '@rushstack/rush-sdk';
import { CommandLineAction } from '@rushstack/ts-command-line';
import { Colorize } from '@rushstack/terminal';
import { AlreadyReportedError, Async, FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library';
import lockfileLintSchema from '../../../schemas/lockfile-lint.schema.json';
import { LOCKFILE_EXPLORER_FOLDERNAME, LOCKFILE_LINT_JSON_FILENAME } from '../../../constants/common';
import { getShrinkwrapFileMajorVersion, parseDependencyPath, splicePackageWithVersion } from '../../../utils/shrinkwrap';
export class CheckAction extends CommandLineAction {
constructor(parser) {
super({
actionName: 'check',
summary: 'Check and report dependency issues in your workspace',
documentation: 'This command applies the policies that are configured in ' +
LOCKFILE_LINT_JSON_FILENAME +
', reporting any problems found in your PNPM workspace.'
});
this._terminal = parser.globalTerminal;
this._checkedProjects = new Set();
this._docMap = new Map();
}
async _checkVersionCompatibilityAsync(shrinkwrapFileMajorVersion, packages, dependencyPath, requiredVersions, checkedDependencyPaths) {
var _a;
if (packages && packages[dependencyPath] && !checkedDependencyPaths.has(dependencyPath)) {
checkedDependencyPaths.add(dependencyPath);
const { name, version } = parseDependencyPath(shrinkwrapFileMajorVersion, dependencyPath);
if (name in requiredVersions && !semver.satisfies(version, requiredVersions[name])) {
throw new Error(`The version of "${name}" should match "${requiredVersions[name]}";` +
` actual version is "${version}"`);
}
await Promise.all(Object.entries((_a = packages[dependencyPath].dependencies) !== null && _a !== void 0 ? _a : {}).map(async ([dependencyPackageName, dependencyPackageVersion]) => {
await this._checkVersionCompatibilityAsync(shrinkwrapFileMajorVersion, packages, splicePackageWithVersion(shrinkwrapFileMajorVersion, dependencyPackageName, dependencyPackageVersion), requiredVersions, checkedDependencyPaths);
}));
}
}
async _searchAndValidateDependenciesAsync(project, requiredVersions) {
this._terminal.writeLine(`Checking project "${project.packageName}"`);
const projectFolder = project.projectFolder;
const subspace = project.subspace;
const shrinkwrapFilename = subspace.getCommittedShrinkwrapFilePath();
let doc;
if (this._docMap.has(shrinkwrapFilename)) {
doc = this._docMap.get(shrinkwrapFilename);
}
else {
const pnpmLockfileText = await FileSystem.readFileAsync(shrinkwrapFilename);
doc = yaml.load(pnpmLockfileText);
this._docMap.set(shrinkwrapFilename, doc);
}
const { importers, lockfileVersion, packages } = doc;
const shrinkwrapFileMajorVersion = getShrinkwrapFileMajorVersion(lockfileVersion);
const checkedDependencyPaths = new Set();
await Promise.all(Object.entries(importers).map(async ([relativePath, { dependencies }]) => {
var _a;
if (path.resolve(projectFolder, relativePath) === projectFolder) {
const dependenciesEntries = Object.entries(dependencies !== null && dependencies !== void 0 ? dependencies : {});
for (const [dependencyName, dependencyValue] of dependenciesEntries) {
const fullDependencyPath = splicePackageWithVersion(shrinkwrapFileMajorVersion, dependencyName, typeof dependencyValue === 'string'
? dependencyValue
: dependencyValue.version);
if (fullDependencyPath.includes('link:')) {
const dependencyProject = this._rushConfiguration.getProjectByName(dependencyName);
if (dependencyProject && !((_a = this._checkedProjects) === null || _a === void 0 ? void 0 : _a.has(dependencyProject))) {
this._checkedProjects.add(project);
await this._searchAndValidateDependenciesAsync(dependencyProject, requiredVersions);
}
}
else {
await this._checkVersionCompatibilityAsync(shrinkwrapFileMajorVersion, packages, fullDependencyPath, requiredVersions, checkedDependencyPaths);
}
}
}
}));
}
async _performVersionRestrictionCheckAsync(requiredVersions, projectName) {
var _a;
try {
const project = (_a = this._rushConfiguration) === null || _a === void 0 ? void 0 : _a.getProjectByName(projectName);
if (!project) {
throw new Error(`Specified project "${projectName}" does not exist in ${LOCKFILE_LINT_JSON_FILENAME}`);
}
this._checkedProjects.add(project);
await this._searchAndValidateDependenciesAsync(project, requiredVersions);
return undefined;
}
catch (e) {
return e.message;
}
}
async onExecuteAsync() {
const rushConfiguration = RushConfiguration.tryLoadFromDefaultLocation();
if (!rushConfiguration) {
throw new Error('The "lockfile-explorer check" must be executed in a folder that is under a Rush workspace folder');
}
this._rushConfiguration = rushConfiguration;
const lintingFile = path.resolve(this._rushConfiguration.commonFolder, 'config', LOCKFILE_EXPLORER_FOLDERNAME, LOCKFILE_LINT_JSON_FILENAME);
const { rules } = await JsonFile.loadAndValidateAsync(lintingFile, JsonSchema.fromLoadedObject(lockfileLintSchema));
const issues = [];
await Async.forEachAsync(rules, async ({ requiredVersions, project, rule }) => {
switch (rule) {
case 'restrict-versions': {
const message = await this._performVersionRestrictionCheckAsync(requiredVersions, project);
if (message) {
issues.push({ project, rule, message });
}
break;
}
default: {
throw new Error('Unsupported rule name: ' + rule);
}
}
}, { concurrency: 50 });
if (issues.length > 0) {
this._terminal.writeLine();
// Deterministic order
for (const issue of issues.sort((a, b) => {
let diff = a.project.localeCompare(b.project);
if (diff !== 0) {
return diff;
}
diff = a.rule.localeCompare(b.rule);
if (diff !== 0) {
return diff;
}
return a.message.localeCompare(b.message);
})) {
this._terminal.writeLine(Colorize.red('PROBLEM: ') + Colorize.cyan(`[${issue.rule}] `) + issue.message + '\n');
}
throw new AlreadyReportedError();
}
this._terminal.writeLine(Colorize.green('SUCCESS: ') + 'All checks passed.');
}
}
//# sourceMappingURL=CheckAction.js.map