UNPKG

@rushstack/lockfile-explorer

Version:

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

138 lines 7.63 kB
// 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