Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions apps/lockfile-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"@types/cors": "~2.8.12",
"@types/express": "4.17.21",
"@types/js-yaml": "4.0.9",
"@types/update-notifier": "~6.0.1",
"eslint": "~9.37.0",
"local-node-rig": "workspace:*",
"@pnpm/lockfile.types": "1002.0.1",
Expand All @@ -70,8 +69,7 @@
"cors": "~2.8.5",
"express": "4.21.1",
"js-yaml": "~4.1.0",
"semver": "~7.7.4",
"update-notifier": "~5.1.0"
"semver": "~7.7.4"
},
"exports": {
"./lib/*.schema.json": "./lib-commonjs/*.schema.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,9 @@ import type { ChildProcess } from 'node:child_process';
import express from 'express';
import yaml from 'js-yaml';
import cors from 'cors';
import updateNotifier from 'update-notifier';

import {
Executable,
FileSystem,
type IPackageJson,
JsonFile,
PackageJsonLookup
} from '@rushstack/node-core-library';
import { ConsoleTerminalProvider, type ITerminal, Terminal, Colorize } from '@rushstack/terminal';
import { Executable, FileSystem, type IPackageJson, JsonFile } from '@rushstack/node-core-library';
import { type ITerminal, Colorize } from '@rushstack/terminal';
import {
type CommandLineFlagParameter,
CommandLineParser,
Expand All @@ -36,17 +29,32 @@ import type { IAppState } from '../../state';
import { init } from '../../utils/init';
import { PnpmfileRunner } from '../../graph/PnpmfileRunner';
import * as lfxGraphLoader from '../../graph/lfxGraphLoader';
import { LFX_PACKAGE_NAME, LFX_VERSION } from '../../utils/constants';
import { type IPackageUpdateResult, PackageUpdateChecker } from '../../utils/PackageUpdateChecker';

const EXPLORER_TOOL_FILENAME: 'lockfile-explorer' = 'lockfile-explorer';

function printUpdateNotification(
result: { latestVersion: string; isOutdated: boolean } | undefined,
terminal: ITerminal
): void {
if (result?.isOutdated) {
terminal.writeLine(
Colorize.yellow(
`\nUpdate available: ${LFX_VERSION} → ${result.latestVersion}\n` +
`Run: npm install -g ${LFX_PACKAGE_NAME}\n`
)
);
}
}

export class ExplorerCommandLineParser extends CommandLineParser {
public readonly globalTerminal: ITerminal;
private readonly _terminalProvider: ConsoleTerminalProvider;
private readonly _debugParameter: CommandLineFlagParameter;

private readonly _debugParameter: CommandLineFlagParameter;
private readonly _subspaceParameter: IRequiredCommandLineStringParameter;

public constructor() {
public constructor(terminal: ITerminal) {
super({
toolFilename: EXPLORER_TOOL_FILENAME,
toolDescription:
Expand All @@ -66,45 +74,37 @@ export class ExplorerCommandLineParser extends CommandLineParser {
defaultValue: 'default'
});

this._terminalProvider = new ConsoleTerminalProvider();
this.globalTerminal = new Terminal(this._terminalProvider);
this.globalTerminal = terminal;
}

public get isDebug(): boolean {
return this._debugParameter.value;
}

protected override async onExecuteAsync(): Promise<void> {
const lockfileExplorerProjectRoot: string = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname)!;
const lockfileExplorerPackageJson: IPackageJson = JsonFile.load(
`${lockfileExplorerProjectRoot}/package.json`
);
const appVersion: string = lockfileExplorerPackageJson.version;
const terminal: ITerminal = this.globalTerminal;

this.globalTerminal.writeLine(
Colorize.bold(`\nRush Lockfile Explorer ${appVersion}`) +
terminal.writeLine(
Colorize.bold(`\nRush Lockfile Explorer ${LFX_VERSION}`) +
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
// Start the update check now so it runs concurrently with server setup.
// The result is awaited and displayed inside app.listen once the server is ready.
const updateChecker: PackageUpdateChecker = new PackageUpdateChecker({
packageName: LFX_PACKAGE_NAME,
currentVersion: LFX_VERSION,
// In debug mode, bypass the cache so the notice appears immediately.
forceCheck: this.isDebug
});
const updateCheckPromise: Promise<IPackageUpdateResult | undefined> = updateChecker.tryGetUpdateAsync();

const PORT: number = 8091;
// Must not have a trailing slash
const SERVICE_URL: string = `http://localhost:${PORT}`;

const appState: IAppState = init({
lockfileExplorerProjectRoot,
appVersion,
appVersion: LFX_VERSION,
debugMode: this.isDebug,
subspaceName: this._subspaceParameter.value
});
Expand All @@ -125,8 +125,8 @@ export class ExplorerCommandLineParser extends CommandLineParser {
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`);
terminal.writeLine(Colorize.red('The client has disconnected!'));
terminal.writeLine(`Please open a browser window at http://localhost:${PORT}/app`);
disconnected = true;
} else if (!awaitingFirstConnect) {
isClientConnected = false;
Expand Down Expand Up @@ -157,7 +157,7 @@ export class ExplorerCommandLineParser extends CommandLineParser {
isClientConnected = true;
if (disconnected) {
disconnected = false;
console.log(Colorize.green('The client has reconnected!'));
terminal.writeLine(Colorize.green('The client has reconnected!'));
}
res.status(200).send();
});
Expand Down Expand Up @@ -253,7 +253,9 @@ export class ExplorerCommandLineParser extends CommandLineParser {
);

app.listen(PORT, async () => {
console.log(`App launched on ${SERVICE_URL}`);
terminal.writeLine(`App launched on ${SERVICE_URL}`);

printUpdateNotification(await updateCheckPromise, terminal);

if (!appState.debugMode) {
try {
Expand Down Expand Up @@ -288,7 +290,7 @@ export class ExplorerCommandLineParser extends CommandLineParser {
// Detach from our Node.js process so the browser stays open after we exit
browserProcess.unref();
} catch (e) {
this.globalTerminal.writeError('Error launching browser: ' + e.toString());
terminal.writeError('Error launching browser: ' + e.toString());
}
}
});
Expand Down
23 changes: 8 additions & 15 deletions apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts
Original file line number Diff line number Diff line change
@@ -1,48 +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, Colorize } from '@rushstack/terminal';
import { type ITerminal, Colorize } from '@rushstack/terminal';
import { CommandLineParser } from '@rushstack/ts-command-line';
import { type IPackageJson, JsonFile, PackageJsonLookup } from '@rushstack/node-core-library';

import { InitAction } from './actions/InitAction';
import { CheckAction } from './actions/CheckAction';
import { LFX_VERSION } from '../../utils/constants';

const LINT_TOOL_FILENAME: 'lockfile-lint' = 'lockfile-lint';

export class LintCommandLineParser extends CommandLineParser {
public readonly globalTerminal: ITerminal;
private readonly _terminalProvider: ConsoleTerminalProvider;

public constructor() {
public constructor(terminal: ITerminal) {
super({
toolFilename: LINT_TOOL_FILENAME,
toolDescription:
'Lockfile Lint applies configured policies to find and report dependency issues in your PNPM workspace.'
});

this._terminalProvider = new ConsoleTerminalProvider();
this.globalTerminal = new Terminal(this._terminalProvider);
this.globalTerminal = terminal;

this._populateActions();
}

protected override async onExecuteAsync(): Promise<void> {
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')
Colorize.bold(`\nRush Lockfile Lint ${LFX_VERSION}`) + Colorize.cyan(' - https://lfx.rushstack.io/\n')
);

await super.onExecuteAsync();
}

private _populateActions(): void {
this.addAction(new InitAction(this));
this.addAction(new CheckAction(this));
const terminal: ITerminal = this.globalTerminal;
this.addAction(new InitAction(terminal));
this.addAction(new CheckAction(terminal));
}
}
5 changes: 2 additions & 3 deletions apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { AlreadyReportedError, Async, FileSystem, JsonFile, JsonSchema } from '@

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,
Expand Down Expand Up @@ -45,7 +44,7 @@ export class CheckAction extends CommandLineAction {
private _checkedProjects: Set<RushConfigurationProject>;
private _docMap: Map<string, lockfileTypes.LockfileObject>;

public constructor(parser: LintCommandLineParser) {
public constructor(terminal: ITerminal) {
super({
actionName: 'check',
summary: 'Check and report dependency issues in your workspace',
Expand All @@ -55,7 +54,7 @@ export class CheckAction extends CommandLineAction {
', reporting any problems found in your PNPM workspace.'
});

this._terminal = parser.globalTerminal;
this._terminal = terminal;
this._checkedProjects = new Set();
this._docMap = new Map();
}
Expand Down
5 changes: 2 additions & 3 deletions apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,20 @@ import { Colorize, type ITerminal } from '@rushstack/terminal';
import { RushConfiguration } from '@rushstack/rush-sdk';
import { FileSystem } from '@rushstack/node-core-library';

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) {
public constructor(terminal: ITerminal) {
super({
actionName: 'init',
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;
this._terminal = terminal;
}

protected override async onExecuteAsync(): Promise<void> {
Expand Down
50 changes: 50 additions & 0 deletions apps/lockfile-explorer/src/cli/test/CommandLineHelp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { AnsiEscape, Terminal, StringBufferTerminalProvider } from '@rushstack/terminal';
import type { CommandLineParser } from '@rushstack/ts-command-line';

import { ExplorerCommandLineParser } from '../explorer/ExplorerCommandLineParser';
import { LintCommandLineParser } from '../lint/LintCommandLineParser';

describe('CommandLineHelp', () => {
let terminal: Terminal;
let terminalProvider: StringBufferTerminalProvider;

beforeEach(() => {
terminalProvider = new StringBufferTerminalProvider();
terminal = new Terminal(terminalProvider);

// ts-command-line calls process.exit() which interferes with Jest
jest.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`Test code called process.exit(${code})`);
});
});

afterEach(() => {
expect(terminalProvider.getAllOutputAsChunks({ asLines: true })).toMatchSnapshot('terminal output');
});

describe.each([
{
name: 'ExplorerCommandLineParser',
createParser: () => new ExplorerCommandLineParser(terminal)
},
{
name: 'LintCommandLineParser',
createParser: () => new LintCommandLineParser(terminal)
}
])('$name', ({ createParser }) => {
it(`prints the help`, async () => {
const parser: CommandLineParser = createParser();

const globalHelpText: string = AnsiEscape.formatForTests(parser.renderHelpText());
expect(globalHelpText).toMatchSnapshot('global help');

for (const action of parser.actions) {
const actionHelpText: string = AnsiEscape.formatForTests(action.renderHelpText());
expect(actionHelpText).toMatchSnapshot(action.actionName);
}
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`CommandLineHelp ExplorerCommandLineParser prints the help: global help 1`] = `
"usage: lockfile-explorer [-h] [-d] [--subspace SUBSPACE_NAME]
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.
-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\\".
[bold]For detailed help about a specific command, use: lockfile-explorer
<command> -h[normal]
"
`;
exports[`CommandLineHelp ExplorerCommandLineParser prints the help: terminal output 1`] = `Array []`;
exports[`CommandLineHelp LintCommandLineParser prints the help: check 1`] = `
"usage: lockfile-lint check [-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.
"
`;
exports[`CommandLineHelp LintCommandLineParser prints the help: global help 1`] = `
"usage: lockfile-lint [-h] <command> ...
Lockfile Lint applies configured policies to find and report dependency
issues in your PNPM workspace.
Positional arguments:
<command>
init Create a new lockfile-lint.json config file
check Check and report dependency issues in your workspace
Optional arguments:
-h, --help Show this help message and exit.
[bold]For detailed help about a specific command, use: lockfile-lint <command>
-h[normal]
"
`;
exports[`CommandLineHelp LintCommandLineParser prints the help: init 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[`CommandLineHelp LintCommandLineParser prints the help: terminal output 1`] = `Array []`;
Loading