From cd7b83086a7c7922c3a70d69494c5ecd5d00a91a Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:08:22 -0700 Subject: [PATCH 01/10] Remove redundant "lockfile-explorer start" subcommand, restoring the old CLI syntax --- .../cli/explorer/ExplorerCommandLineParser.ts | 225 ++++++++++++++++- .../src/cli/explorer/actions/StartAction.ts | 235 ------------------ .../src/test/__snapshots__/help.test.ts.snap | 15 +- 3 files changed, 223 insertions(+), 252 deletions(-) delete mode 100644 apps/lockfile-explorer/src/cli/explorer/actions/StartAction.ts diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index 278c085465f..2f77a1a1e64 100644 --- a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -1,10 +1,26 @@ // 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 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 { StartAction } from './actions/StartAction'; +import { FileSystem, type IPackageJson, JsonFile, PackageJsonLookup } from '@rushstack/node-core-library'; +import { ConsoleTerminalProvider, type ITerminal, Terminal, Colorize } from '@rushstack/terminal'; +import { + type CommandLineFlagParameter, + CommandLineParser, + type IRequiredCommandLineStringParameter +} from '@rushstack/ts-command-line'; +import type { IAppContext } from '@rushstack/lockfile-explorer-web/lib/AppContext'; +import type { Lockfile } from '@pnpm/lockfile-types'; + +import type { IAppState } from '../../state'; +import { init } from '../../utils/init'; +import { convertLockfileV6DepPathToV5DepPath, getShrinkwrapFileMajorVersion } from '../../utils/shrinkwrap'; const EXPLORER_TOOL_FILENAME: 'lockfile-explorer' = 'lockfile-explorer'; @@ -13,6 +29,8 @@ export class ExplorerCommandLineParser extends CommandLineParser { private readonly _terminalProvider: ConsoleTerminalProvider; private readonly _debugParameter: CommandLineFlagParameter; + private readonly _subspaceParameter: IRequiredCommandLineStringParameter; + public constructor() { super({ toolFilename: EXPLORER_TOOL_FILENAME, @@ -25,17 +43,206 @@ export class ExplorerCommandLineParser extends CommandLineParser { description: 'Show the full call stack if an error occurs while executing the tool' }); + this._subspaceParameter = this.defineStringParameter({ + parameterLongName: '--subspace', + argumentName: 'SUBSPACE_NAME', + description: 'Specifies an individual Rush subspace to check.', + defaultValue: 'default' + }); + 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; } + + 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.globalTerminal.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.globalTerminal.writeError('Error launching browser: ' + e.toString()); + } + } + }); + } } diff --git a/apps/lockfile-explorer/src/cli/explorer/actions/StartAction.ts b/apps/lockfile-explorer/src/cli/explorer/actions/StartAction.ts deleted file mode 100644 index 216a4e4de5b..00000000000 --- a/apps/lockfile-explorer/src/cli/explorer/actions/StartAction.ts +++ /dev/null @@ -1,235 +0,0 @@ -// 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/test/__snapshots__/help.test.ts.snap b/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap index fc17d8c426f..7a66d376c5c 100644 --- a/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap +++ b/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap @@ -1,18 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CLI Tool Tests should display help for "lockfile-explorer --help" 1`] = ` -"usage: lockfile-explorer [-h] [-d] ... +"usage: lockfile-explorer [-h] [-d] [--subspace SUBSPACE_NAME] 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 + -h, --help Show this help message and exit. + -d, --debug Show the full call stack if an error occurs while + executing the tool + --subspace SUBSPACE_NAME + Specifies an individual Rush subspace to check. The + default value is \\"default\\". For detailed help about a specific command, use: lockfile-explorer -h From 928a95cc495e69bede358305028c2864059f5e07 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:20:37 -0700 Subject: [PATCH 02/10] Improve help text --- .../cli/explorer/ExplorerCommandLineParser.ts | 3 +- .../src/cli/lint/LintCommandLineParser.ts | 3 +- .../src/cli/lint/actions/InitAction.ts | 6 ++-- .../src/cli/lint/actions/LintAction.ts | 7 ++-- .../src/test/__snapshots__/help.test.ts.snap | 33 ++++++++++++++++--- apps/lockfile-explorer/src/test/help.test.ts | 10 ++++++ 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index 2f77a1a1e64..c7b2a8e25db 100644 --- a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -34,7 +34,8 @@ export class ExplorerCommandLineParser extends CommandLineParser { public constructor() { super({ toolFilename: EXPLORER_TOOL_FILENAME, - toolDescription: 'lockfile-lint is a tool for linting lockfiles.' + toolDescription: + 'Lockfile Explorer is a desktop app for investigating and solving version conflicts in a PNPM workspace.' }); this._debugParameter = this.defineFlagParameter({ diff --git a/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts index ec1153f23b3..01e826e6655 100644 --- a/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts @@ -15,7 +15,8 @@ export class LintCommandLineParser extends CommandLineParser { public constructor() { super({ toolFilename: LINT_TOOL_FILENAME, - toolDescription: 'lockfile-lint is a tool for linting lockfiles.' + toolDescription: + 'Lockfile Lint applies configured policies to find and report dependency issues in your PNPM workspace.' }); this._terminalProvider = new ConsoleTerminalProvider(); diff --git a/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts index 12b9e9c60c0..4b23a8f73b1 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts @@ -17,8 +17,10 @@ export class InitAction extends CommandLineAction { public constructor(parser: LintCommandLineParser) { super({ actionName: 'init', - summary: `Create ${LOCKFILE_LINT_JSON_FILENAME} config file`, - documentation: `Create ${LOCKFILE_LINT_JSON_FILENAME} config file` + summary: `Create a new ${LOCKFILE_LINT_JSON_FILENAME} config file`, + documentation: + `This command initializes a new ${LOCKFILE_LINT_JSON_FILENAME} config file.` + + ` The created template file includes source code comments that document the settings.` }); this._terminal = parser.globalTerminal; } diff --git a/apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts index acd74c4ac01..406da095e75 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts @@ -39,8 +39,11 @@ export class LintAction extends CommandLineAction { 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' + 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; diff --git a/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap b/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap index 7a66d376c5c..8ec3bf7ecd0 100644 --- a/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap +++ b/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap @@ -3,7 +3,8 @@ exports[`CLI Tool Tests should display help for "lockfile-explorer --help" 1`] = ` "usage: lockfile-explorer [-h] [-d] [--subspace SUBSPACE_NAME] -lockfile-lint is a tool for linting lockfiles. +Lockfile Explorer is a desktop app for investigating and solving version +conflicts in a PNPM workspace. Optional arguments: -h, --help Show this help message and exit. @@ -21,13 +22,13 @@ Optional arguments: exports[`CLI Tool Tests should display help for "lockfile-lint --help" 1`] = ` "usage: lockfile-lint [-h] ... -lockfile-lint is a tool for linting lockfiles. +Lockfile Lint applies configured policies to find and report dependency +issues in your PNPM workspace. Positional arguments: - init Create lockfile-lint.json config file - lint Check if the specified package has a inconsistent package - versions in target project + init Create a new lockfile-lint.json config file + lint Check and report dependency issues in your workspace Optional arguments: -h, --help Show this help message and exit. @@ -36,3 +37,25 @@ Optional arguments: -h " `; + +exports[`CLI Tool Tests should display help for "lockfile-lint init --help" 1`] = ` +"usage: lockfile-lint init [-h] + +This command initializes a new lockfile-lint.json config file. The created +template file includes source code comments that document the settings. + +Optional arguments: + -h, --help Show this help message and exit. +" +`; + +exports[`CLI Tool Tests should display help for "lockfile-lint lint --help" 1`] = ` +"usage: lockfile-lint lint [-h] + +This command applies the policies that are configured in lockfile-lint.json, +reporting any problems found in your PNPM workspace. + +Optional arguments: + -h, --help Show this help message and exit. +" +`; diff --git a/apps/lockfile-explorer/src/test/help.test.ts b/apps/lockfile-explorer/src/test/help.test.ts index 00d1196df49..d4d30301f22 100644 --- a/apps/lockfile-explorer/src/test/help.test.ts +++ b/apps/lockfile-explorer/src/test/help.test.ts @@ -13,4 +13,14 @@ describe('CLI Tool Tests', () => { const lintOutput = execSync('node lib/start-lint.js --help').toString(); expect(lintOutput).toMatchSnapshot(); }); + + it('should display help for "lockfile-lint init --help"', () => { + const lintOutput = execSync('node lib/start-lint.js init --help').toString(); + expect(lintOutput).toMatchSnapshot(); + }); + + it('should display help for "lockfile-lint lint --help"', () => { + const lintOutput = execSync('node lib/start-lint.js lint --help').toString(); + expect(lintOutput).toMatchSnapshot(); + }); }); From 8e31a0e7c9d81b96c5afda491cd23502a242d206 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:25:35 -0700 Subject: [PATCH 03/10] Rename "lockfile-lint lint" to "lockfile-lint check" to be less redundant --- .../src/cli/lint/LintCommandLineParser.ts | 4 ++-- .../actions/{LintAction.ts => CheckAction.ts} | 4 ++-- .../src/test/__snapshots__/help.test.ts.snap | 18 +++++++++--------- apps/lockfile-explorer/src/test/help.test.ts | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) rename apps/lockfile-explorer/src/cli/lint/actions/{LintAction.ts => CheckAction.ts} (98%) diff --git a/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts index 01e826e6655..e1356a7516b 100644 --- a/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts @@ -4,7 +4,7 @@ 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'; +import { CheckAction } from './actions/CheckAction'; const LINT_TOOL_FILENAME: 'lockfile-lint' = 'lockfile-lint'; @@ -27,6 +27,6 @@ export class LintCommandLineParser extends CommandLineParser { private _populateActions(): void { this.addAction(new InitAction(this)); - this.addAction(new LintAction(this)); + this.addAction(new CheckAction(this)); } } diff --git a/apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts similarity index 98% rename from apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts rename to apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts index 406da095e75..6a65adb0563 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/LintAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts @@ -29,7 +29,7 @@ export interface ILockfileLint { rules: ILintRule[]; } -export class LintAction extends CommandLineAction { +export class CheckAction extends CommandLineAction { private readonly _terminal: ITerminal; private _rushConfiguration!: RushConfiguration; @@ -38,7 +38,7 @@ export class LintAction extends CommandLineAction { public constructor(parser: LintCommandLineParser) { super({ - actionName: 'lint', + actionName: 'check', summary: 'Check and report dependency issues in your workspace', documentation: 'This command applies the policies that are configured in ' + diff --git a/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap b/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap index 8ec3bf7ecd0..f9a9e3e57eb 100644 --- a/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap +++ b/apps/lockfile-explorer/src/test/__snapshots__/help.test.ts.snap @@ -28,7 +28,7 @@ issues in your PNPM workspace. Positional arguments: init Create a new lockfile-lint.json config file - lint Check and report dependency issues in your workspace + check Check and report dependency issues in your workspace Optional arguments: -h, --help Show this help message and exit. @@ -38,22 +38,22 @@ Optional arguments: " `; -exports[`CLI Tool Tests should display help for "lockfile-lint init --help" 1`] = ` -"usage: lockfile-lint init [-h] +exports[`CLI Tool Tests should display help for "lockfile-lint check --help" 1`] = ` +"usage: lockfile-lint check [-h] -This command initializes a new lockfile-lint.json config file. The created -template file includes source code comments that document the settings. +This command applies the policies that are configured in lockfile-lint.json, +reporting any problems found in your PNPM workspace. Optional arguments: -h, --help Show this help message and exit. " `; -exports[`CLI Tool Tests should display help for "lockfile-lint lint --help" 1`] = ` -"usage: lockfile-lint lint [-h] +exports[`CLI Tool Tests should display help for "lockfile-lint init --help" 1`] = ` +"usage: lockfile-lint init [-h] -This command applies the policies that are configured in lockfile-lint.json, -reporting any problems found in your PNPM workspace. +This command initializes a new lockfile-lint.json config file. The created +template file includes source code comments that document the settings. Optional arguments: -h, --help Show this help message and exit. diff --git a/apps/lockfile-explorer/src/test/help.test.ts b/apps/lockfile-explorer/src/test/help.test.ts index d4d30301f22..3832ac674d8 100644 --- a/apps/lockfile-explorer/src/test/help.test.ts +++ b/apps/lockfile-explorer/src/test/help.test.ts @@ -19,8 +19,8 @@ describe('CLI Tool Tests', () => { expect(lintOutput).toMatchSnapshot(); }); - it('should display help for "lockfile-lint lint --help"', () => { - const lintOutput = execSync('node lib/start-lint.js lint --help').toString(); + it('should display help for "lockfile-lint check --help"', () => { + const lintOutput = execSync('node lib/start-lint.js check --help').toString(); expect(lintOutput).toMatchSnapshot(); }); }); From 86ca9e2675ddceaf45028845d89af06a46604058 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:59:59 -0700 Subject: [PATCH 04/10] Avoid double-printing errors when AlreadyReportedError is thrown --- libraries/ts-command-line/src/TypeUuidLite.ts | 29 +++++++++++++++++++ .../src/providers/CommandLineParser.ts | 6 ++++ 2 files changed, 35 insertions(+) create mode 100644 libraries/ts-command-line/src/TypeUuidLite.ts diff --git a/libraries/ts-command-line/src/TypeUuidLite.ts b/libraries/ts-command-line/src/TypeUuidLite.ts new file mode 100644 index 00000000000..9898e51f6b2 --- /dev/null +++ b/libraries/ts-command-line/src/TypeUuidLite.ts @@ -0,0 +1,29 @@ +const classPrototypeUuidSymbol: symbol = Symbol.for('TypeUuid.classPrototypeUuid'); + +export const uuidAlreadyReportedError: string = 'f26b0640-a49b-49d1-9ead-1a516d5920c7'; + +// Avoid a dependency on node-core-library to access just this one API: +export class TypeUuid { + /** + * Returns true if the `targetObject` is an instance of a JavaScript class that was previously + * registered using the specified `typeUuid`. Base classes are also considered. + */ + public static isInstanceOf(targetObject: unknown, typeUuid: string): boolean { + if (targetObject === undefined || targetObject === null) { + return false; + } + + let objectPrototype: {} = Object.getPrototypeOf(targetObject); + while (objectPrototype !== undefined && objectPrototype !== null) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const registeredUuid: string = (objectPrototype as any)[classPrototypeUuidSymbol]; + if (registeredUuid === typeUuid) { + return true; + } + // Walk upwards an examine base class prototypes + objectPrototype = Object.getPrototypeOf(objectPrototype); + } + + return false; + } +} diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index f7d2a1311f4..3d76239f7bd 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -13,6 +13,7 @@ import { } from './CommandLineParameterProvider'; import { CommandLineParserExitError, CustomArgumentParser } from './CommandLineParserExitError'; import { TabCompleteAction } from './TabCompletionAction'; +import { TypeUuid, uuidAlreadyReportedError } from '../TypeUuidLite'; /** * Options for the {@link CommandLineParser} constructor. @@ -167,6 +168,11 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { if (!process.exitCode) { process.exitCode = err.exitCode; } + } else if (TypeUuid.isInstanceOf(err, uuidAlreadyReportedError)) { + // AlreadyReportedError + if (!process.exitCode) { + process.exitCode = 1; + } } else { let message: string = ((err as Error).message || 'An unknown error occurred').trim(); From e7d14f223a84eabb63f8925db4a579b8d4940242 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:13:29 -0700 Subject: [PATCH 05/10] Improve success/failure output --- .../src/cli/lint/actions/CheckAction.ts | 37 ++++++++++++++----- libraries/ts-command-line/src/TypeUuidLite.ts | 3 ++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts index 6a65adb0563..79ce0b77ae5 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts @@ -29,6 +29,11 @@ export interface ILockfileLint { rules: ILintRule[]; } +export interface ILintIssue { + rule: string; + message: string; +} + export class CheckAction extends CommandLineAction { private readonly _terminal: ITerminal; @@ -62,7 +67,10 @@ export class CheckAction extends CommandLineAction { 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}'!`); + throw new Error( + `The version of "${name}" should match "${requiredVersions[name]}";` + + ` actual version is "${version}"` + ); } await Promise.all( @@ -89,7 +97,7 @@ export class CheckAction extends CommandLineAction { project: RushConfigurationProject, requiredVersions: Record ): Promise { - this._terminal.writeLine(`Checking the project: ${project.packageName}.`); + this._terminal.writeLine(`\nChecking project "${project.packageName}"...\n`); const projectFolder: string = project.projectFolder; const subspace: Subspace = project.subspace; @@ -148,7 +156,7 @@ export class CheckAction extends CommandLineAction { private async _performVersionRestrictionCheckAsync( requiredVersions: Record, projectName: string - ): Promise { + ): Promise { try { const project: RushConfigurationProject | undefined = this._rushConfiguration?.getProjectByName(projectName); @@ -159,6 +167,7 @@ export class CheckAction extends CommandLineAction { } this._checkedProjects.add(project); await this._searchAndValidateDependenciesAsync(project, requiredVersions); + return undefined; } catch (e) { return e.message; } @@ -183,15 +192,18 @@ export class CheckAction extends CommandLineAction { lintingFile, JsonSchema.fromLoadedObject(lockfileLintSchema) ); - const errorMessageList: string[] = []; + const issues: ILintIssue[] = []; 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); + const message: string | undefined = await this._performVersionRestrictionCheckAsync( + requiredVersions, + project + ); + if (message) { + issues.push({ rule, message }); } break; } @@ -203,10 +215,15 @@ export class CheckAction extends CommandLineAction { }, { concurrency: 50 } ); - if (errorMessageList.length > 0) { - this._terminal.writeError(errorMessageList.join('\n')); + if (issues.length > 0) { + for (const issue of issues) { + this._terminal.writeLine( + Colorize.red('PROBLEM: ') + Colorize.cyan(`[${issue.rule}] `) + issue.message + '\n' + ); + } + throw new AlreadyReportedError(); } - this._terminal.writeLine(Colorize.green('Check passed!')); + this._terminal.writeLine(Colorize.green('SUCCESS: ') + 'All checks passed.'); } } diff --git a/libraries/ts-command-line/src/TypeUuidLite.ts b/libraries/ts-command-line/src/TypeUuidLite.ts index 9898e51f6b2..6915073bac0 100644 --- a/libraries/ts-command-line/src/TypeUuidLite.ts +++ b/libraries/ts-command-line/src/TypeUuidLite.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + const classPrototypeUuidSymbol: symbol = Symbol.for('TypeUuid.classPrototypeUuid'); export const uuidAlreadyReportedError: string = 'f26b0640-a49b-49d1-9ead-1a516d5920c7'; From fd18ed06dcbf829aeb5a08a8f4b4bf83381d3f97 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 22:00:43 -0700 Subject: [PATCH 06/10] Improve output formatting --- .../src/cli/lint/LintCommandLineParser.ts | 17 +++++++++++++++- .../src/cli/lint/actions/CheckAction.ts | 20 ++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts index e1356a7516b..782c7e6859c 100644 --- a/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts @@ -1,10 +1,11 @@ // 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 { ConsoleTerminalProvider, type ITerminal, Terminal, Colorize } from '@rushstack/terminal'; import { CommandLineParser } from '@rushstack/ts-command-line'; import { InitAction } from './actions/InitAction'; import { CheckAction } from './actions/CheckAction'; +import { type IPackageJson, JsonFile, PackageJsonLookup } from '@rushstack/node-core-library'; const LINT_TOOL_FILENAME: 'lockfile-lint' = 'lockfile-lint'; @@ -25,6 +26,20 @@ export class LintCommandLineParser extends CommandLineParser { this._populateActions(); } + protected override async onExecute(): Promise { + const lockfileExplorerProjectRoot: string = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname)!; + const lockfileExplorerPackageJson: IPackageJson = JsonFile.load( + `${lockfileExplorerProjectRoot}/package.json` + ); + const appVersion: string = lockfileExplorerPackageJson.version; + + this.globalTerminal.writeLine( + Colorize.bold(`\nRush Lockfile Lint ${appVersion}`) + Colorize.cyan(' - https://lfx.rushstack.io/\n') + ); + + await super.onExecute(); + } + private _populateActions(): void { this.addAction(new InitAction(this)); this.addAction(new CheckAction(this)); diff --git a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts index 79ce0b77ae5..e63bec03e7d 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts @@ -30,6 +30,7 @@ export interface ILockfileLint { } export interface ILintIssue { + project: string; rule: string; message: string; } @@ -97,7 +98,7 @@ export class CheckAction extends CommandLineAction { project: RushConfigurationProject, requiredVersions: Record ): Promise { - this._terminal.writeLine(`\nChecking project "${project.packageName}"...\n`); + this._terminal.writeLine(`Checking project "${project.packageName}"`); const projectFolder: string = project.projectFolder; const subspace: Subspace = project.subspace; @@ -203,7 +204,7 @@ export class CheckAction extends CommandLineAction { project ); if (message) { - issues.push({ rule, message }); + issues.push({ project, rule, message }); } break; } @@ -216,7 +217,20 @@ export class CheckAction extends CommandLineAction { { concurrency: 50 } ); if (issues.length > 0) { - for (const issue of issues) { + this._terminal.writeLine(); + + // Deterministic order + for (const issue of issues.sort((a, b): number => { + let diff: number = 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' ); From bc19e279769eed97f5de488854f5af76d4301af5 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 22:01:06 -0700 Subject: [PATCH 07/10] rush change --- .../octogonz-lfx-fixes_2024-06-25-05-00.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/ts-command-line/octogonz-lfx-fixes_2024-06-25-05-00.json diff --git a/common/changes/@rushstack/ts-command-line/octogonz-lfx-fixes_2024-06-25-05-00.json b/common/changes/@rushstack/ts-command-line/octogonz-lfx-fixes_2024-06-25-05-00.json new file mode 100644 index 00000000000..a03064b455f --- /dev/null +++ b/common/changes/@rushstack/ts-command-line/octogonz-lfx-fixes_2024-06-25-05-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/ts-command-line", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/ts-command-line" +} \ No newline at end of file From 7f12506838904978e1b35f406c14af44b0b5da78 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 22:09:22 -0700 Subject: [PATCH 08/10] rush update --- common/config/subspaces/build-tests-subspace/repo-state.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index 7fe3bc80994..cad5a171e53 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -2,5 +2,5 @@ { "pnpmShrinkwrapHash": "c040a0d59aada7e1f9bdf0916df7079547de3a85", "preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648", - "packageJsonInjectedDependenciesHash": "76d0e3cf15d68a8ef680fbbfa7cbe1c0683b5c5b" + "packageJsonInjectedDependenciesHash": "6a2ceee7ad225052456350348e8c71e6f8f2ffea" } From ed18824af6e7cfb698f87ded68d2bc520bdc373f Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 22:11:59 -0700 Subject: [PATCH 09/10] Reenable publishing --- rush.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rush.json b/rush.json index e70b30c238d..5d01e0a3693 100644 --- a/rush.json +++ b/rush.json @@ -409,8 +409,7 @@ "packageName": "@rushstack/lockfile-explorer", "projectFolder": "apps/lockfile-explorer", "reviewCategory": "libraries", - // Temporarily suspend publishing while we investigate some issues - "shouldPublish": false + "shouldPublish": true }, { "packageName": "@rushstack/lockfile-explorer-web", From d72d66dc841f61e25cc2e3640711ed7b95570d7e Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Mon, 24 Jun 2024 22:20:51 -0700 Subject: [PATCH 10/10] rush change --- .../octogonz-lfx-fixes_2024-06-25-05-18.json | 10 ++++++++++ .../octogonz-lfx-fixes_2024-06-25-05-00.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes_2024-06-25-05-18.json diff --git a/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes_2024-06-25-05-18.json b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes_2024-06-25-05-18.json new file mode 100644 index 00000000000..22d17643a68 --- /dev/null +++ b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes_2024-06-25-05-18.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/lockfile-explorer", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/lockfile-explorer" +} \ No newline at end of file diff --git a/common/changes/@rushstack/ts-command-line/octogonz-lfx-fixes_2024-06-25-05-00.json b/common/changes/@rushstack/ts-command-line/octogonz-lfx-fixes_2024-06-25-05-00.json index a03064b455f..93d330b4d3a 100644 --- a/common/changes/@rushstack/ts-command-line/octogonz-lfx-fixes_2024-06-25-05-00.json +++ b/common/changes/@rushstack/ts-command-line/octogonz-lfx-fixes_2024-06-25-05-00.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@rushstack/ts-command-line", - "comment": "", + "comment": "Improve the `CommandLineParser` error reporting to handle `AlreadyReportedError` from `@rushstack/node-core-library`", "type": "none" } ],