diff --git a/apps/lockfile-explorer/bin/lockfile-explorer b/apps/lockfile-explorer/bin/lockfile-explorer index aee68e80224..55f974d6de8 100755 --- a/apps/lockfile-explorer/bin/lockfile-explorer +++ b/apps/lockfile-explorer/bin/lockfile-explorer @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('../lib/start.js'); +require('../lib/start-explorer.js'); diff --git a/apps/lockfile-explorer/bin/lockfile-lint b/apps/lockfile-explorer/bin/lockfile-lint new file mode 100644 index 00000000000..44c9dbf8bdd --- /dev/null +++ b/apps/lockfile-explorer/bin/lockfile-lint @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/start-lint.js'); diff --git a/apps/lockfile-explorer/package.json b/apps/lockfile-explorer/package.json index 17f672abff2..24240e981ad 100644 --- a/apps/lockfile-explorer/package.json +++ b/apps/lockfile-explorer/package.json @@ -26,7 +26,9 @@ "license": "MIT", "bin": { "lockfile-explorer": "./bin/lockfile-explorer", - "lfx": "./bin/lockfile-explorer" + "lfx": "./bin/lockfile-explorer", + "lockfile-lint": "./bin/lockfile-lint", + "lflint": "./bin/lockfile-lint" }, "scripts": { "build": "heft build --clean", @@ -52,7 +54,8 @@ "@types/js-yaml": "3.12.1", "@types/update-notifier": "~6.0.1", "local-node-rig": "workspace:*", - "@pnpm/lockfile-types": "~6.0.0" + "@pnpm/lockfile-types": "^5.1.5", + "@types/semver": "7.5.0" }, "dependencies": { "@microsoft/rush-lib": "workspace:*", @@ -63,6 +66,9 @@ "js-yaml": "~3.13.1", "open": "~8.4.0", "update-notifier": "~5.1.0", - "@pnpm/dependency-path": "~2.1.2" + "@pnpm/dependency-path": "~2.1.2", + "semver": "~7.5.4", + "@rushstack/rush-sdk": "workspace:*", + "@rushstack/ts-command-line": "workspace:*" } } diff --git a/apps/lockfile-explorer/src/assets/lint-init/lockfile-lint-template.json b/apps/lockfile-explorer/src/assets/lint-init/lockfile-lint-template.json new file mode 100644 index 00000000000..72449103987 --- /dev/null +++ b/apps/lockfile-explorer/src/assets/lint-init/lockfile-lint-template.json @@ -0,0 +1,42 @@ +/** + * Config file for Lockfile Lint. For more info, please visit: https://lfx.rushstack.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/lockfile-explorer/lockfile-lint.schema.json", + + /** + * The list of rules to be checked by Lockfile Lint. For each rule configuration, the + * type of rule is determined by the `rule` field. + */ + "rules": [ + // /** + // * The `restrict-versions` rule enforces that direct and indirect dependencies must + // * satisfy a specified version range. + // */ + // { + // "rule": "restrict-versions", + // + // /** + // * The name of a workspace project to analyze. + // */ + // "project": "@my-company/my-app", + // + // /** + // * Indicates the package versions to be checked. The `requiredVersions` key is + // * the name of an NPM package, and the value is a SemVer range. If the project has + // * that NPM package as a dependency, then its version must satisfy the SemVer range. + // * This check also applies to devDependencies and peerDependencies, as well as any + // * indirect dependencies of the project. + // */ + // "requiredVersions": { + // /** + // * For example, if `react-router` appears anywhere in the dependency graph of + // * `@my-company/my-app`, then it must be version 5 or 6. + // */ + // "react-router": "5.x || 6.x", + // "react": "^18.3.0", + // "react-dom": "^18.3.0" + // } + // } + ] +} diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts new file mode 100644 index 00000000000..278c085465f --- /dev/null +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ConsoleTerminalProvider, type ITerminal, Terminal } from '@rushstack/terminal'; +import { type CommandLineFlagParameter, CommandLineParser } from '@rushstack/ts-command-line'; + +import { StartAction } from './actions/StartAction'; + +const EXPLORER_TOOL_FILENAME: 'lockfile-explorer' = 'lockfile-explorer'; + +export class ExplorerCommandLineParser extends CommandLineParser { + public readonly globalTerminal: ITerminal; + private readonly _terminalProvider: ConsoleTerminalProvider; + private readonly _debugParameter: CommandLineFlagParameter; + + public constructor() { + super({ + toolFilename: EXPLORER_TOOL_FILENAME, + toolDescription: 'lockfile-lint is a tool for linting lockfiles.' + }); + + this._debugParameter = this.defineFlagParameter({ + parameterLongName: '--debug', + parameterShortName: '-d', + description: 'Show the full call stack if an error occurs while executing the tool' + }); + + this._terminalProvider = new ConsoleTerminalProvider(); + this.globalTerminal = new Terminal(this._terminalProvider); + + this._populateActions(); + } + + private _populateActions(): void { + this.addAction(new StartAction(this)); + } + + public get isDebug(): boolean { + return this._debugParameter.value; + } +} diff --git a/apps/lockfile-explorer/src/cli/explorer/actions/StartAction.ts b/apps/lockfile-explorer/src/cli/explorer/actions/StartAction.ts new file mode 100644 index 00000000000..216a4e4de5b --- /dev/null +++ b/apps/lockfile-explorer/src/cli/explorer/actions/StartAction.ts @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ITerminal } from '@rushstack/terminal'; +import { CommandLineAction, type IRequiredCommandLineStringParameter } from '@rushstack/ts-command-line'; +import express from 'express'; +import yaml from 'js-yaml'; +import cors from 'cors'; +import process from 'process'; +import open from 'open'; +import updateNotifier from 'update-notifier'; +import { FileSystem, type IPackageJson, JsonFile, PackageJsonLookup } from '@rushstack/node-core-library'; +import type { IAppContext } from '@rushstack/lockfile-explorer-web/lib/AppContext'; +import { Colorize } from '@rushstack/terminal'; +import type { Lockfile } from '@pnpm/lockfile-types'; + +import { init } from '../../../utils/init'; +import type { IAppState } from '../../../state'; +import { + convertLockfileV6DepPathToV5DepPath, + getShrinkwrapFileMajorVersion +} from '../../../utils/shrinkwrap'; +import type { ExplorerCommandLineParser } from '../ExplorerCommandLineParser'; + +export class StartAction extends CommandLineAction { + private readonly _terminal: ITerminal; + private readonly _isDebug: boolean; + private readonly _subspaceParameter: IRequiredCommandLineStringParameter; + + public constructor(parser: ExplorerCommandLineParser) { + super({ + actionName: 'start', + summary: 'Start the application', + documentation: 'Start the application' + }); + + this._subspaceParameter = this.defineStringParameter({ + parameterLongName: '--subspace', + argumentName: 'SUBSPACE_NAME', + description: 'Specifies an individual Rush subspace to check.', + defaultValue: 'default' + }); + + this._terminal = parser.globalTerminal; + this._isDebug = parser.isDebug; + } + + protected async onExecute(): Promise { + const lockfileExplorerProjectRoot: string = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname)!; + const lockfileExplorerPackageJson: IPackageJson = JsonFile.load( + `${lockfileExplorerProjectRoot}/package.json` + ); + const appVersion: string = lockfileExplorerPackageJson.version; + + this._terminal.writeLine( + Colorize.bold(`\nRush Lockfile Explorer ${appVersion}`) + + Colorize.cyan(' - https://lfx.rushstack.io/\n') + ); + + updateNotifier({ + pkg: lockfileExplorerPackageJson, + // Normally update-notifier waits a day or so before it starts displaying upgrade notices. + // In debug mode, show the notice right away. + updateCheckInterval: this._isDebug ? 0 : undefined + }).notify({ + // Make sure it says "-g" in the "npm install" example command line + isGlobal: true, + // Show the notice immediately, rather than waiting for process.onExit() + defer: false + }); + + const PORT: number = 8091; + // Must not have a trailing slash + const SERVICE_URL: string = `http://localhost:${PORT}`; + + const appState: IAppState = init({ + lockfileExplorerProjectRoot, + appVersion, + debugMode: this._isDebug, + subspaceName: this._subspaceParameter.value + }); + + // Important: This must happen after init() reads the current working directory + process.chdir(appState.lockfileExplorerProjectRoot); + + const distFolderPath: string = `${appState.lockfileExplorerProjectRoot}/dist`; + const app: express.Application = express(); + app.use(express.json()); + app.use(cors()); + + // Variable used to check if the front-end client is still connected + let awaitingFirstConnect: boolean = true; + let isClientConnected: boolean = false; + let disconnected: boolean = false; + setInterval(() => { + if (!isClientConnected && !awaitingFirstConnect && !disconnected) { + console.log(Colorize.red('The client has disconnected!')); + console.log(`Please open a browser window at http://localhost:${PORT}/app`); + disconnected = true; + } else if (!awaitingFirstConnect) { + isClientConnected = false; + } + }, 4000); + + // This takes precedence over the `/app` static route, which also has an `initappcontext.js` file. + app.get('/initappcontext.js', (req: express.Request, res: express.Response) => { + const appContext: IAppContext = { + serviceUrl: SERVICE_URL, + appVersion: appState.appVersion, + debugMode: this._isDebug + }; + const sourceCode: string = [ + `console.log('Loaded initappcontext.js');`, + `appContext = ${JSON.stringify(appContext)}` + ].join('\n'); + + res.type('application/javascript').send(sourceCode); + }); + + app.use('/', express.static(distFolderPath)); + + app.use('/favicon.ico', express.static(distFolderPath, { index: 'favicon.ico' })); + + app.get('/api/lockfile', async (req: express.Request, res: express.Response) => { + const pnpmLockfileText: string = await FileSystem.readFileAsync(appState.pnpmLockfileLocation); + const doc = yaml.load(pnpmLockfileText) as Lockfile; + const { packages, lockfileVersion } = doc; + + const shrinkwrapFileMajorVersion: number = getShrinkwrapFileMajorVersion(lockfileVersion); + + if (packages && shrinkwrapFileMajorVersion === 6) { + const updatedPackages: Lockfile['packages'] = {}; + for (const [dependencyPath, dependency] of Object.entries(packages)) { + updatedPackages[convertLockfileV6DepPathToV5DepPath(dependencyPath)] = dependency; + } + doc.packages = updatedPackages; + } + + res.send({ + doc, + subspaceName: this._subspaceParameter.value + }); + }); + + app.get('/api/health', (req: express.Request, res: express.Response) => { + awaitingFirstConnect = false; + isClientConnected = true; + if (disconnected) { + disconnected = false; + console.log(Colorize.green('The client has reconnected!')); + } + res.status(200).send(); + }); + + app.post( + '/api/package-json', + async (req: express.Request<{}, {}, { projectPath: string }, {}>, res: express.Response) => { + const { projectPath } = req.body; + const fileLocation = `${appState.projectRoot}/${projectPath}/package.json`; + let packageJsonText: string; + try { + packageJsonText = await FileSystem.readFileAsync(fileLocation); + } catch (e) { + if (FileSystem.isNotExistError(e)) { + return res.status(404).send({ + message: `Could not load package.json file for this package. Have you installed all the dependencies for this workspace?`, + error: `No package.json in location: ${projectPath}` + }); + } else { + throw e; + } + } + + res.send(packageJsonText); + } + ); + + app.get('/api/pnpmfile', async (req: express.Request, res: express.Response) => { + let pnpmfile: string; + try { + pnpmfile = await FileSystem.readFileAsync(appState.pnpmfileLocation); + } catch (e) { + if (FileSystem.isNotExistError(e)) { + return res.status(404).send({ + message: `Could not load pnpmfile file in this repo.`, + error: `No .pnpmifile.cjs found.` + }); + } else { + throw e; + } + } + + res.send(pnpmfile); + }); + + app.post( + '/api/package-spec', + async (req: express.Request<{}, {}, { projectPath: string }, {}>, res: express.Response) => { + const { projectPath } = req.body; + const fileLocation = `${appState.projectRoot}/${projectPath}/package.json`; + let packageJson: IPackageJson; + try { + packageJson = await JsonFile.loadAsync(fileLocation); + } catch (e) { + if (FileSystem.isNotExistError(e)) { + return res.status(404).send({ + message: `Could not load package.json file in location: ${projectPath}` + }); + } else { + throw e; + } + } + + const { + hooks: { readPackage } + } = require(appState.pnpmfileLocation); + const parsedPackage = readPackage(packageJson, {}); + res.send(parsedPackage); + } + ); + + app.listen(PORT, async () => { + console.log(`App launched on ${SERVICE_URL}`); + + if (!appState.debugMode) { + try { + // Launch the web browser + await open(SERVICE_URL); + } catch (e) { + this._terminal.writeError('Error launching browser: ' + e.toString()); + } + } + }); + } +} diff --git a/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts new file mode 100644 index 00000000000..ec1153f23b3 --- /dev/null +++ b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ConsoleTerminalProvider, type ITerminal, Terminal } from '@rushstack/terminal'; +import { CommandLineParser } from '@rushstack/ts-command-line'; +import { InitAction } from './actions/InitAction'; +import { LintAction } from './actions/LintAction'; + +const LINT_TOOL_FILENAME: 'lockfile-lint' = 'lockfile-lint'; + +export class LintCommandLineParser extends CommandLineParser { + public readonly globalTerminal: ITerminal; + private readonly _terminalProvider: ConsoleTerminalProvider; + + public constructor() { + super({ + toolFilename: LINT_TOOL_FILENAME, + toolDescription: 'lockfile-lint is a tool for linting lockfiles.' + }); + + this._terminalProvider = new ConsoleTerminalProvider(); + this.globalTerminal = new Terminal(this._terminalProvider); + + this._populateActions(); + } + + private _populateActions(): void { + this.addAction(new InitAction(this)); + this.addAction(new LintAction(this)); + } +} diff --git a/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts new file mode 100644 index 00000000000..12b9e9c60c0 --- /dev/null +++ b/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { CommandLineAction } from '@rushstack/ts-command-line'; + +import { Colorize, type ITerminal } from '@rushstack/terminal'; +import { RushConfiguration } from '@rushstack/rush-sdk'; +import { FileSystem } from '@rushstack/node-core-library'; +import path from 'path'; + +import type { LintCommandLineParser } from '../LintCommandLineParser'; +import { LOCKFILE_EXPLORER_FOLDERNAME, LOCKFILE_LINT_JSON_FILENAME } from '../../../constants/common'; + +export class InitAction extends CommandLineAction { + private readonly _terminal: ITerminal; + + public constructor(parser: LintCommandLineParser) { + super({ + actionName: 'init', + summary: `Create ${LOCKFILE_LINT_JSON_FILENAME} config file`, + documentation: `Create ${LOCKFILE_LINT_JSON_FILENAME} config file` + }); + this._terminal = parser.globalTerminal; + } + + protected async onExecute(): Promise { + const rushConfiguration: RushConfiguration | undefined = RushConfiguration.tryLoadFromDefaultLocation(); + if (!rushConfiguration) { + throw new Error( + 'The "lockfile-explorer check" must be executed in a folder that is under a Rush workspace folder' + ); + } + const inputFilePath: string = path.resolve( + __dirname, + '../../../assets/lint-init/lockfile-lint-template.json' + ); + const outputFilePath: string = path.resolve( + rushConfiguration.commonFolder, + 'config', + LOCKFILE_EXPLORER_FOLDERNAME, + LOCKFILE_LINT_JSON_FILENAME + ); + + if (await FileSystem.existsAsync(outputFilePath)) { + this._terminal.writeError('The output file already exists:'); + this._terminal.writeLine('\n ' + outputFilePath + '\n'); + throw new Error('Unable to write output file'); + } + + this._terminal.writeLine(Colorize.green('Writing file: ') + outputFilePath); + await FileSystem.copyFileAsync({ + sourcePath: inputFilePath, + destinationPath: outputFilePath + }); + } +} diff --git a/apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts new file mode 100644 index 00000000000..acd74c4ac01 --- /dev/null +++ b/apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Colorize, type ITerminal } from '@rushstack/terminal'; +import { CommandLineAction } from '@rushstack/ts-command-line'; +import { RushConfiguration, type RushConfigurationProject, type Subspace } from '@rushstack/rush-sdk'; +import path from 'path'; +import yaml from 'js-yaml'; +import semver from 'semver'; +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 type { LintCommandLineParser } from '../LintCommandLineParser'; +import { + getShrinkwrapFileMajorVersion, + parseDependencyPath, + splicePackageWithVersion +} from '../../../utils/shrinkwrap'; +import type { Lockfile, LockfileV6 } from '@pnpm/lockfile-types'; + +export interface ILintRule { + rule: 'restrict-versions'; + project: string; + requiredVersions: Record; +} + +export interface ILockfileLint { + rules: ILintRule[]; +} + +export class LintAction extends CommandLineAction { + private readonly _terminal: ITerminal; + + private _rushConfiguration!: RushConfiguration; + private _checkedProjects: Set; + private _docMap: Map; + + public constructor(parser: LintCommandLineParser) { + super({ + actionName: 'lint', + summary: 'Check if the specified package has a inconsistent package versions in target project', + documentation: 'Check if the specified package has a inconsistent package versions in target project' + }); + + this._terminal = parser.globalTerminal; + this._checkedProjects = new Set(); + this._docMap = new Map(); + } + + private async _checkVersionCompatibilityAsync( + shrinkwrapFileMajorVersion: number, + packages: Lockfile['packages'], + dependencyPath: string, + requiredVersions: Record, + checkedDependencyPaths: Set + ): Promise { + 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(`ERROR: Detected inconsistent version numbers in package '${name}': '${version}'!`); + } + + await Promise.all( + Object.entries(packages[dependencyPath].dependencies ?? {}).map( + async ([dependencyPackageName, dependencyPackageVersion]) => { + await this._checkVersionCompatibilityAsync( + shrinkwrapFileMajorVersion, + packages, + splicePackageWithVersion( + shrinkwrapFileMajorVersion, + dependencyPackageName, + dependencyPackageVersion + ), + requiredVersions, + checkedDependencyPaths + ); + } + ) + ); + } + } + + private async _searchAndValidateDependenciesAsync( + project: RushConfigurationProject, + requiredVersions: Record + ): Promise { + this._terminal.writeLine(`Checking the project: ${project.packageName}.`); + + const projectFolder: string = project.projectFolder; + const subspace: Subspace = project.subspace; + const shrinkwrapFilename: string = subspace.getCommittedShrinkwrapFilename(); + let doc: Lockfile | LockfileV6; + if (this._docMap.has(shrinkwrapFilename)) { + doc = this._docMap.get(shrinkwrapFilename)!; + } else { + const pnpmLockfileText: string = await FileSystem.readFileAsync(shrinkwrapFilename); + doc = yaml.load(pnpmLockfileText) as Lockfile | LockfileV6; + this._docMap.set(shrinkwrapFilename, doc); + } + const { importers, lockfileVersion, packages } = doc; + const shrinkwrapFileMajorVersion: number = getShrinkwrapFileMajorVersion(lockfileVersion); + const checkedDependencyPaths: Set = new Set(); + + await Promise.all( + Object.entries(importers).map(async ([relativePath, { dependencies }]) => { + if (path.resolve(projectFolder, relativePath) === projectFolder) { + const dependenciesEntries = Object.entries(dependencies ?? {}); + for (const [dependencyName, dependencyValue] of dependenciesEntries) { + const fullDependencyPath = splicePackageWithVersion( + shrinkwrapFileMajorVersion, + dependencyName, + typeof dependencyValue === 'string' + ? dependencyValue + : ( + dependencyValue as { + version: string; + specifier: string; + } + ).version + ); + if (fullDependencyPath.includes('link:')) { + const dependencyProject: RushConfigurationProject | undefined = + this._rushConfiguration.getProjectByName(dependencyName); + if (dependencyProject && !this._checkedProjects?.has(dependencyProject)) { + this._checkedProjects!.add(project); + await this._searchAndValidateDependenciesAsync(dependencyProject, requiredVersions); + } + } else { + await this._checkVersionCompatibilityAsync( + shrinkwrapFileMajorVersion, + packages, + fullDependencyPath, + requiredVersions, + checkedDependencyPaths + ); + } + } + } + }) + ); + } + + private async _performVersionRestrictionCheckAsync( + requiredVersions: Record, + projectName: string + ): Promise { + try { + const project: RushConfigurationProject | undefined = + this._rushConfiguration?.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); + } catch (e) { + return e.message; + } + } + + protected async onExecute(): Promise { + const rushConfiguration: RushConfiguration | undefined = 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: string = path.resolve( + this._rushConfiguration.commonFolder, + 'config', + LOCKFILE_EXPLORER_FOLDERNAME, + LOCKFILE_LINT_JSON_FILENAME + ); + const { rules }: ILockfileLint = await JsonFile.loadAndValidateAsync( + lintingFile, + JsonSchema.fromLoadedObject(lockfileLintSchema) + ); + const errorMessageList: string[] = []; + await Async.forEachAsync( + rules, + async ({ requiredVersions, project, rule }) => { + switch (rule) { + case 'restrict-versions': { + const errorMessage = await this._performVersionRestrictionCheckAsync(requiredVersions, project); + if (errorMessage) { + errorMessageList.push(errorMessage); + } + break; + } + + default: { + throw new Error('Unsupported rule name: ' + rule); + } + } + }, + { concurrency: 50 } + ); + if (errorMessageList.length > 0) { + this._terminal.writeError(errorMessageList.join('\n')); + throw new AlreadyReportedError(); + } + this._terminal.writeLine(Colorize.green('Check passed!')); + } +} diff --git a/apps/lockfile-explorer/src/commandLine.test.ts b/apps/lockfile-explorer/src/commandLine.test.ts deleted file mode 100644 index e42babe89a5..00000000000 --- a/apps/lockfile-explorer/src/commandLine.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { parseCommandLine } from './commandLine'; - -describe('commandLine', () => { - describe('parseCommandLine()', () => { - it('sets showHelp when --help specified', async () => { - const result = parseCommandLine(['--help']); - expect(result).toHaveProperty('showedHelp', true); - }); - - it('sets subspace when --subspace specified', async () => { - const result = parseCommandLine(['--subspace', 'wallet']); - expect(result).toHaveProperty('subspace', 'wallet'); - }); - - it('sets error when --subspace value not missing', async () => { - const result = parseCommandLine(['--subspace']); - expect(result).toHaveProperty('error', 'Expecting argument after "--subspace"'); - }); - }); -}); diff --git a/apps/lockfile-explorer/src/commandLine.ts b/apps/lockfile-explorer/src/commandLine.ts deleted file mode 100644 index 9a7d1bb9a2a..00000000000 --- a/apps/lockfile-explorer/src/commandLine.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -export interface ICommandLine { - showedHelp: boolean; - error: string | undefined; - subspace: string | undefined; -} - -function showHelp(): void { - console.log( - ` -Usage: lockfile-explorer [--subspace SUBSPACE] - lockfile-explorer [--help] - -Launches the Lockfile Explorer app. You can also use "lfx" as shorthand alias -for "lockfile-explorer". - -Parameters: - ---help, -h - Show command line help - ---subspace SUBSPACE, -s SUBSPACE - Load the lockfile for the specified Rush subspace. -`.trim() - ); -} - -export function parseCommandLine(args: string[]): ICommandLine { - const result: ICommandLine = { showedHelp: false, error: undefined, subspace: undefined }; - - let i: number = 0; - - while (i < args.length) { - const parameter: string = args[i]; - ++i; - - switch (parameter) { - case '--help': - case '-h': - case '/?': - showHelp(); - result.showedHelp = true; - return result; - - case '--subspace': - case '-s': - if (i >= args.length || args[i].startsWith('-')) { - result.error = `Expecting argument after "${parameter}"`; - return result; - } - result.subspace = args[i]; - ++i; - break; - - default: - result.error = 'Unknown parameter ' + JSON.stringify(parameter); - return result; - } - } - - return result; -} diff --git a/apps/lockfile-explorer/src/constants/common.ts b/apps/lockfile-explorer/src/constants/common.ts new file mode 100644 index 00000000000..37e8a75b1c3 --- /dev/null +++ b/apps/lockfile-explorer/src/constants/common.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export const LOCKFILE_LINT_JSON_FILENAME: 'lockfile-lint.json' = 'lockfile-lint.json'; + +export const LOCKFILE_EXPLORER_FOLDERNAME: 'lockfile-explorer' = 'lockfile-explorer'; diff --git a/apps/lockfile-explorer/src/schemas/lockfile-lint.schema.json b/apps/lockfile-explorer/src/schemas/lockfile-lint.schema.json new file mode 100644 index 00000000000..0e383a91bd1 --- /dev/null +++ b/apps/lockfile-explorer/src/schemas/lockfile-lint.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Lockfile Lint Configuration", + "description": "The lockfile-explorer.json configuration file for lockfile-lint tool.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "description": "Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.", + "type": "string" + }, + "rules": { + "description": "The rules adopted by Monorepo and the lockfile-lint will help to check.", + "type": "array", + "items": { + "type": "object", + "oneOf": [ + { + "type": "object", + "required": ["rule", "project", "requiredVersions"], + "properties": { + "rule": { + "description": "Rule name applied to the project.", + "const": "restrict-versions" + }, + "project": { + "description": "Project name.", + "type": "string" + }, + "requiredVersions": { + "description": "List of restrict dependency version.", + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + } + } + } + } + ] + } + } + } +} diff --git a/apps/lockfile-explorer/src/start-explorer.ts b/apps/lockfile-explorer/src/start-explorer.ts new file mode 100644 index 00000000000..217080ff9d5 --- /dev/null +++ b/apps/lockfile-explorer/src/start-explorer.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ExplorerCommandLineParser } from './cli/explorer/ExplorerCommandLineParser'; + +const parser: ExplorerCommandLineParser = new ExplorerCommandLineParser(); + +parser.executeAsync().catch(console.error); // CommandLineParser.executeAsync() should never reject the promise diff --git a/apps/lockfile-explorer/src/start-lint.ts b/apps/lockfile-explorer/src/start-lint.ts new file mode 100644 index 00000000000..f911d04dfde --- /dev/null +++ b/apps/lockfile-explorer/src/start-lint.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { LintCommandLineParser } from './cli/lint/LintCommandLineParser'; + +const parser: LintCommandLineParser = new LintCommandLineParser(); + +parser.executeAsync().catch(console.error); // CommandLineParser.executeAsync() should never reject the promise diff --git a/apps/lockfile-explorer/src/start.ts b/apps/lockfile-explorer/src/start.ts deleted file mode 100644 index 724d0fbd593..00000000000 --- a/apps/lockfile-explorer/src/start.ts +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import express from 'express'; -import yaml from 'js-yaml'; -import cors from 'cors'; -import process from 'process'; -import open from 'open'; -import updateNotifier from 'update-notifier'; -import { AlreadyReportedError } from '@rushstack/node-core-library'; -import { FileSystem, type IPackageJson, JsonFile, PackageJsonLookup } from '@rushstack/node-core-library'; -import type { IAppContext } from '@rushstack/lockfile-explorer-web/lib/AppContext'; -import { Colorize } from '@rushstack/terminal'; -import type { Lockfile } from '@pnpm/lockfile-types'; - -import { convertLockfileV6DepPathToV5DepPath } from './utils'; -import { init } from './init'; -import type { IAppState } from './state'; -import { type ICommandLine, parseCommandLine } from './commandLine'; - -function startApp(debugMode: boolean): void { - const lockfileExplorerProjectRoot: string = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname)!; - const lockfileExplorerPackageJson: IPackageJson = JsonFile.load( - `${lockfileExplorerProjectRoot}/package.json` - ); - const appVersion: string = lockfileExplorerPackageJson.version; - - console.log( - Colorize.bold(`\nRush Lockfile Explorer ${appVersion}`) + Colorize.cyan(' - https://lfx.rushstack.io/\n') - ); - - updateNotifier({ - pkg: lockfileExplorerPackageJson, - // Normally update-notifier waits a day or so before it starts displaying upgrade notices. - // In debug mode, show the notice right away. - updateCheckInterval: debugMode ? 0 : undefined - }).notify({ - // Make sure it says "-g" in the "npm install" example command line - isGlobal: true, - // Show the notice immediately, rather than waiting for process.onExit() - defer: false - }); - - const PORT: number = 8091; - // Must not have a trailing slash - const SERVICE_URL: string = `http://localhost:${PORT}`; - - const result: ICommandLine = parseCommandLine(process.argv.slice(2)); - if (result.showedHelp) { - return; - } - - if (result.error) { - console.error('\n' + Colorize.red('ERROR: ' + result.error)); - console.error('\nFor help, use: ' + Colorize.yellow('lockfile-explorer --help')); - process.exitCode = 1; - return; - } - - const subspaceName: string = result.subspace ?? 'default'; - - const appState: IAppState = init({ lockfileExplorerProjectRoot, appVersion, debugMode, subspaceName }); - - // Important: This must happen after init() reads the current working directory - process.chdir(appState.lockfileExplorerProjectRoot); - - const distFolderPath: string = `${appState.lockfileExplorerProjectRoot}/dist`; - const app: express.Application = express(); - app.use(express.json()); - app.use(cors()); - - // Variable used to check if the front-end client is still connected - let awaitingFirstConnect: boolean = true; - let isClientConnected: boolean = false; - let disconnected: boolean = false; - setInterval(() => { - if (!isClientConnected && !awaitingFirstConnect && !disconnected) { - console.log(Colorize.red('The client has disconnected!')); - console.log(`Please open a browser window at http://localhost:${PORT}/app`); - disconnected = true; - } else if (!awaitingFirstConnect) { - isClientConnected = false; - } - }, 4000); - - // This takes precedence over the `/app` static route, which also has an `initappcontext.js` file. - app.get('/initappcontext.js', (req: express.Request, res: express.Response) => { - const appContext: IAppContext = { - serviceUrl: SERVICE_URL, - appVersion: appState.appVersion, - debugMode: process.argv.indexOf('--debug') >= 0 - }; - const sourceCode: string = [ - `console.log('Loaded initappcontext.js');`, - `appContext = ${JSON.stringify(appContext)}` - ].join('\n'); - - res.type('application/javascript').send(sourceCode); - }); - - app.use('/', express.static(distFolderPath)); - - app.use('/favicon.ico', express.static(distFolderPath, { index: 'favicon.ico' })); - - app.get('/api/lockfile', async (req: express.Request, res: express.Response) => { - const pnpmLockfileText: string = await FileSystem.readFileAsync(appState.pnpmLockfileLocation); - const doc = yaml.load(pnpmLockfileText) as Lockfile; - const { packages, lockfileVersion } = doc; - - let shrinkwrapFileMajorVersion: number; - if (typeof lockfileVersion === 'string') { - const isDotIncluded: boolean = lockfileVersion.includes('.'); - shrinkwrapFileMajorVersion = parseInt( - lockfileVersion.substring(0, isDotIncluded ? lockfileVersion.indexOf('.') : undefined), - 10 - ); - } else if (typeof lockfileVersion === 'number') { - shrinkwrapFileMajorVersion = Math.floor(lockfileVersion); - } else { - shrinkwrapFileMajorVersion = 0; - } - - if (shrinkwrapFileMajorVersion < 5 || shrinkwrapFileMajorVersion > 6) { - throw new Error('The current lockfile version is not supported.'); - } - - if (packages && shrinkwrapFileMajorVersion === 6) { - const updatedPackages: Lockfile['packages'] = {}; - for (const [dependencyPath, dependency] of Object.entries(packages)) { - updatedPackages[convertLockfileV6DepPathToV5DepPath(dependencyPath)] = dependency; - } - doc.packages = updatedPackages; - } - - res.send({ - doc, - subspaceName - }); - }); - - app.get('/api/health', (req: express.Request, res: express.Response) => { - awaitingFirstConnect = false; - isClientConnected = true; - if (disconnected) { - disconnected = false; - console.log(Colorize.green('The client has reconnected!')); - } - res.status(200).send(); - }); - - app.post( - '/api/package-json', - async (req: express.Request<{}, {}, { projectPath: string }, {}>, res: express.Response) => { - const { projectPath } = req.body; - const fileLocation = `${appState.projectRoot}/${projectPath}/package.json`; - let packageJsonText: string; - try { - packageJsonText = await FileSystem.readFileAsync(fileLocation); - } catch (e) { - if (FileSystem.isNotExistError(e)) { - return res.status(404).send({ - message: `Could not load package.json file for this package. Have you installed all the dependencies for this workspace?`, - error: `No package.json in location: ${projectPath}` - }); - } else { - throw e; - } - } - - res.send(packageJsonText); - } - ); - - app.get('/api/pnpmfile', async (req: express.Request, res: express.Response) => { - let pnpmfile: string; - try { - pnpmfile = await FileSystem.readFileAsync(appState.pnpmfileLocation); - } catch (e) { - if (FileSystem.isNotExistError(e)) { - return res.status(404).send({ - message: `Could not load pnpmfile file in this repo.`, - error: `No .pnpmifile.cjs found.` - }); - } else { - throw e; - } - } - - res.send(pnpmfile); - }); - - app.post( - '/api/package-spec', - async (req: express.Request<{}, {}, { projectPath: string }, {}>, res: express.Response) => { - const { projectPath } = req.body; - const fileLocation = `${appState.projectRoot}/${projectPath}/package.json`; - let packageJson: IPackageJson; - try { - packageJson = await JsonFile.loadAsync(fileLocation); - } catch (e) { - if (FileSystem.isNotExistError(e)) { - return res.status(404).send({ - message: `Could not load package.json file in location: ${projectPath}` - }); - } else { - throw e; - } - } - - const { - hooks: { readPackage } - } = require(appState.pnpmfileLocation); - const parsedPackage = readPackage(packageJson, {}); - res.send(parsedPackage); - } - ); - - app.listen(PORT, async () => { - console.log(`App launched on ${SERVICE_URL}`); - - if (!appState.debugMode) { - try { - // Launch the web browser - await open(SERVICE_URL); - } catch (e) { - console.error('Error launching browser: ' + e.toString()); - } - } - }); -} - -const debugMode: boolean = process.argv.indexOf('--debug') >= 0; -if (debugMode) { - // Display the full callstack for errors - startApp(debugMode); -} else { - // Catch exceptions and report them nicely - try { - startApp(debugMode); - } catch (error) { - if (!(error instanceof AlreadyReportedError)) { - console.error(); - console.error(Colorize.red('ERROR: ' + error.message)); - } - } -} diff --git a/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap b/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap new file mode 100644 index 00000000000..fc17d8c426f --- /dev/null +++ b/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CLI Tool Tests should display help for "lockfile-explorer --help" 1`] = ` +"usage: lockfile-explorer [-h] [-d] ... + +lockfile-lint is a tool for linting lockfiles. + +Positional arguments: + + start Start the application + +Optional arguments: + -h, --help Show this help message and exit. + -d, --debug Show the full call stack if an error occurs while executing + the tool + +For detailed help about a specific command, use: lockfile-explorer + -h +" +`; + +exports[`CLI Tool Tests should display help for "lockfile-lint --help" 1`] = ` +"usage: lockfile-lint [-h] ... + +lockfile-lint is a tool for linting lockfiles. + +Positional arguments: + + init Create lockfile-lint.json config file + lint Check if the specified package has a inconsistent package + versions in target project + +Optional arguments: + -h, --help Show this help message and exit. + +For detailed help about a specific command, use: lockfile-lint +-h +" +`; diff --git a/apps/lockfile-explorer/src/test/help.test.ts b/apps/lockfile-explorer/src/test/help.test.ts new file mode 100644 index 00000000000..00d1196df49 --- /dev/null +++ b/apps/lockfile-explorer/src/test/help.test.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { execSync } from 'child_process'; + +describe('CLI Tool Tests', () => { + it('should display help for "lockfile-explorer --help"', () => { + const startOutput = execSync('node lib/start-explorer.js --help').toString(); + expect(startOutput).toMatchSnapshot(); + }); + + it('should display help for "lockfile-lint --help"', () => { + const lintOutput = execSync('node lib/start-lint.js --help').toString(); + expect(lintOutput).toMatchSnapshot(); + }); +}); diff --git a/apps/lockfile-explorer/src/utils.ts b/apps/lockfile-explorer/src/utils.ts deleted file mode 100644 index 9f5c5f60af4..00000000000 --- a/apps/lockfile-explorer/src/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as dp from '@pnpm/dependency-path'; - -export function convertLockfileV6DepPathToV5DepPath(newDepPath: string): string { - if (!newDepPath.includes('@', 2) || newDepPath.startsWith('file:')) return newDepPath; - const index = newDepPath.indexOf('@', newDepPath.indexOf('/@') + 2); - if (newDepPath.includes('(') && index > dp.indexOfPeersSuffix(newDepPath)) return newDepPath; - return `${newDepPath.substring(0, index)}/${newDepPath.substring(index + 1)}`; -} diff --git a/apps/lockfile-explorer/src/init.ts b/apps/lockfile-explorer/src/utils/init.ts similarity index 99% rename from apps/lockfile-explorer/src/init.ts rename to apps/lockfile-explorer/src/utils/init.ts index 96514eaac44..f6822eb5cc9 100644 --- a/apps/lockfile-explorer/src/init.ts +++ b/apps/lockfile-explorer/src/utils/init.ts @@ -9,7 +9,7 @@ import { RushConfiguration } from '@microsoft/rush-lib/lib/api/RushConfiguration import type { Subspace } from '@microsoft/rush-lib/lib/api/Subspace'; import path from 'path'; -import { type IAppState, type IRushProjectDetails, ProjectType } from './state'; +import { type IAppState, type IRushProjectDetails, ProjectType } from '../state'; export const init = (options: { lockfileExplorerProjectRoot: string; diff --git a/apps/lockfile-explorer/src/utils/shrinkwrap.ts b/apps/lockfile-explorer/src/utils/shrinkwrap.ts new file mode 100644 index 00000000000..3d96e44b4e2 --- /dev/null +++ b/apps/lockfile-explorer/src/utils/shrinkwrap.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as dp from '@pnpm/dependency-path'; + +interface IPackageInfo { + name: string; + peersSuffix: string | undefined; + version: string; +} + +export function convertLockfileV6DepPathToV5DepPath(newDepPath: string): string { + if (!newDepPath.includes('@', 2) || newDepPath.startsWith('file:')) return newDepPath; + const index = newDepPath.indexOf('@', newDepPath.indexOf('/@') + 2); + if (newDepPath.includes('(') && index > dp.indexOfPeersSuffix(newDepPath)) return newDepPath; + return `${newDepPath.substring(0, index)}/${newDepPath.substring(index + 1)}`; +} + +export function parseDependencyPath(shrinkwrapFileMajorVersion: number, newDepPath: string): IPackageInfo { + let dependencyPath: string = newDepPath; + if (shrinkwrapFileMajorVersion === 6) { + dependencyPath = convertLockfileV6DepPathToV5DepPath(newDepPath); + } + const packageInfo = dp.parse(dependencyPath); + return { + name: packageInfo.name as string, + peersSuffix: packageInfo.peersSuffix, + version: packageInfo.version as string + }; +} + +export function getShrinkwrapFileMajorVersion(lockfileVersion: string | number): number { + let shrinkwrapFileMajorVersion: number; + if (typeof lockfileVersion === 'string') { + const isDotIncluded: boolean = lockfileVersion.includes('.'); + shrinkwrapFileMajorVersion = parseInt( + lockfileVersion.substring(0, isDotIncluded ? lockfileVersion.indexOf('.') : undefined), + 10 + ); + } else if (typeof lockfileVersion === 'number') { + shrinkwrapFileMajorVersion = Math.floor(lockfileVersion); + } else { + shrinkwrapFileMajorVersion = 0; + } + + if (shrinkwrapFileMajorVersion < 5 || shrinkwrapFileMajorVersion > 6) { + throw new Error('The current lockfile version is not supported.'); + } + + return shrinkwrapFileMajorVersion; +} + +export function splicePackageWithVersion( + shrinkwrapFileMajorVersion: number, + dependencyPackageName: string, + dependencyPackageVersion: string +): string { + return `/${dependencyPackageName}${shrinkwrapFileMajorVersion === 6 ? '@' : '/'}${dependencyPackageVersion}`; +} diff --git a/common/changes/@rushstack/lockfile-explorer/main_2024-05-15-14-28.json b/common/changes/@rushstack/lockfile-explorer/main_2024-05-15-14-28.json new file mode 100644 index 00000000000..1f5372a14cb --- /dev/null +++ b/common/changes/@rushstack/lockfile-explorer/main_2024-05-15-14-28.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/lockfile-explorer", + "comment": "Add new \"lockfile-lint\" command", + "type": "minor" + } + ], + "packageName": "@rushstack/lockfile-explorer" +} \ No newline at end of file diff --git a/common/config/lockfile-explorer/lockfile-lint.json b/common/config/lockfile-explorer/lockfile-lint.json new file mode 100644 index 00000000000..72449103987 --- /dev/null +++ b/common/config/lockfile-explorer/lockfile-lint.json @@ -0,0 +1,42 @@ +/** + * Config file for Lockfile Lint. For more info, please visit: https://lfx.rushstack.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/lockfile-explorer/lockfile-lint.schema.json", + + /** + * The list of rules to be checked by Lockfile Lint. For each rule configuration, the + * type of rule is determined by the `rule` field. + */ + "rules": [ + // /** + // * The `restrict-versions` rule enforces that direct and indirect dependencies must + // * satisfy a specified version range. + // */ + // { + // "rule": "restrict-versions", + // + // /** + // * The name of a workspace project to analyze. + // */ + // "project": "@my-company/my-app", + // + // /** + // * Indicates the package versions to be checked. The `requiredVersions` key is + // * the name of an NPM package, and the value is a SemVer range. If the project has + // * that NPM package as a dependency, then its version must satisfy the SemVer range. + // * This check also applies to devDependencies and peerDependencies, as well as any + // * indirect dependencies of the project. + // */ + // "requiredVersions": { + // /** + // * For example, if `react-router` appears anywhere in the dependency graph of + // * `@my-company/my-app`, then it must be version 5 or 6. + // */ + // "react-router": "5.x || 6.x", + // "react": "^18.3.0", + // "react-dom": "^18.3.0" + // } + // } + ] +} diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index bcebb849426..b1eb3c9d3e0 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -198,9 +198,15 @@ importers: '@rushstack/node-core-library': specifier: workspace:* version: link:../../libraries/node-core-library + '@rushstack/rush-sdk': + specifier: workspace:* + version: link:../../libraries/rush-sdk '@rushstack/terminal': specifier: workspace:* version: link:../../libraries/terminal + '@rushstack/ts-command-line': + specifier: workspace:* + version: link:../../libraries/ts-command-line cors: specifier: ~2.8.5 version: 2.8.5 @@ -213,13 +219,16 @@ importers: open: specifier: ~8.4.0 version: 8.4.2 + semver: + specifier: ~7.5.4 + version: 7.5.4 update-notifier: specifier: ~5.1.0 version: 5.1.0 devDependencies: '@pnpm/lockfile-types': - specifier: ~6.0.0 - version: 6.0.0 + specifier: ^5.1.5 + version: 5.1.5 '@rushstack/heft': specifier: workspace:* version: link:../heft @@ -235,6 +244,9 @@ importers: '@types/js-yaml': specifier: 3.12.1 version: 3.12.1 + '@types/semver': + specifier: 7.5.0 + version: 7.5.0 '@types/update-notifier': specifier: ~6.0.1 version: 6.0.8 @@ -9508,11 +9520,11 @@ packages: ramda: 0.27.2 dev: false - /@pnpm/lockfile-types@6.0.0: - resolution: {integrity: sha512-a4/ULIPLZIIq8Qmi2HEoFgRTtEouGU5RNhuGDxnSmkxu1BjlNMNjLJeEI5jzMZCGOjBoML+AirY/XOO3bcEQ/w==} - engines: {node: '>=18.12'} + /@pnpm/lockfile-types@5.1.5: + resolution: {integrity: sha512-02FP0HynzX+2DcuPtuMy7PH+kLIC0pevAydAOK+zug2bwdlSLErlvSkc+4+3dw60eRWgUXUqyfO2eR/Ansdbng==} + engines: {node: '>=16.14'} dependencies: - '@pnpm/types': 10.0.0 + '@pnpm/types': 9.4.2 dev: true /@pnpm/logger@4.0.0: @@ -9567,11 +9579,6 @@ packages: strip-bom: 4.0.0 dev: false - /@pnpm/types@10.0.0: - resolution: {integrity: sha512-P608MRTOExt5BkIN2hsrb/ycEchwaPW/x80ujJUAqxKZSXNVAOrlEu3KJ+2+jTCunyWmo/EcE01ZdwCw8jgVrQ==} - engines: {node: '>=18.12'} - dev: true - /@pnpm/types@6.4.0: resolution: {integrity: sha512-nco4+4sZqNHn60Y4VE/fbtlShCBqipyUO+nKRPvDHqLrecMW9pzHWMVRxk4nrMRoeowj3q0rX3GYRBa8lsHTAg==} engines: {node: '>=10.16'} @@ -9585,7 +9592,6 @@ packages: /@pnpm/types@9.4.2: resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} engines: {node: '>=16.14'} - dev: false /@pnpm/write-project-manifest@1.1.7: resolution: {integrity: sha512-OLkDZSqkA1mkoPNPvLFXyI6fb0enCuFji6Zfditi/CLAo9kmIhQFmEUDu4krSB8i908EljG8YwL5Xjxzm5wsWA==} diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 387215603b5..41d6ad4536b 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "476f2603f99c5002946e2800e4f271c60c97cc3e", + "pnpmShrinkwrapHash": "3ba3d9b61d250362588cd82fe943afaf67ec0612", "preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648" }