From bc70e24970e8099d9d892c475e4197de4ef455a0 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sat, 18 Apr 2026 14:52:44 -0500 Subject: [PATCH 1/5] Extract package.json stuff into a constants file. --- .../cli/explorer/ExplorerCommandLineParser.ts | 8 +------- apps/lockfile-explorer/src/utils/constants.ts | 15 +++++++++++++++ apps/lockfile-explorer/src/utils/init.ts | 14 ++++++++------ .../src/utils/test/constants.test.ts | 16 ++++++++++++++++ 4 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 apps/lockfile-explorer/src/utils/constants.ts create mode 100644 apps/lockfile-explorer/src/utils/test/constants.test.ts diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index b1e38d3ee5a..62060b64b62 100644 --- a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -75,11 +75,6 @@ export class ExplorerCommandLineParser extends CommandLineParser { } protected override async onExecuteAsync(): 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}`) + @@ -103,8 +98,7 @@ export class ExplorerCommandLineParser extends CommandLineParser { const SERVICE_URL: string = `http://localhost:${PORT}`; const appState: IAppState = init({ - lockfileExplorerProjectRoot, - appVersion, + appVersion: LFX_VERSION, debugMode: this.isDebug, subspaceName: this._subspaceParameter.value }); diff --git a/apps/lockfile-explorer/src/utils/constants.ts b/apps/lockfile-explorer/src/utils/constants.ts new file mode 100644 index 00000000000..a0eb0883b85 --- /dev/null +++ b/apps/lockfile-explorer/src/utils/constants.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Path } from '@rushstack/node-core-library'; + +import lockfileExplorerPackageJson from '../../package.json'; + +export const { version: LFX_VERSION, name: LFX_PACKAGE_NAME } = lockfileExplorerPackageJson; + +let _lfxPackageRoot: string = Path.convertToSlashes(__dirname); +_lfxPackageRoot = _lfxPackageRoot.slice( + 0, + _lfxPackageRoot.lastIndexOf('/', _lfxPackageRoot.lastIndexOf('/') - 1) +); +export const LFX_PACKAGE_ROOT: string = Path.convertToPlatformDefault(_lfxPackageRoot); diff --git a/apps/lockfile-explorer/src/utils/init.ts b/apps/lockfile-explorer/src/utils/init.ts index 0fe6c7f9eb6..375caf0cd2c 100644 --- a/apps/lockfile-explorer/src/utils/init.ts +++ b/apps/lockfile-explorer/src/utils/init.ts @@ -12,14 +12,16 @@ import type { Subspace } from '@microsoft/rush-lib/lib/api/Subspace'; import * as lockfilePath from '../graph/lockfilePath'; import type { IAppState } from '../state'; +import { LFX_PACKAGE_ROOT } from './constants'; -export const init = (options: { - lockfileExplorerProjectRoot: string; +export interface IInitOptions { appVersion: string; debugMode: boolean; subspaceName: string; -}): Omit => { - const { lockfileExplorerProjectRoot, appVersion, debugMode, subspaceName } = options; +} + +export const init = (options: IInitOptions): Omit => { + const { appVersion, debugMode, subspaceName } = options; const currentWorkingDirectory: string = path.resolve(process.cwd()); let appState: IAppState | undefined; @@ -51,7 +53,7 @@ export const init = (options: { currentWorkingDirectory, appVersion, debugMode, - lockfileExplorerProjectRoot, + lockfileExplorerProjectRoot: LFX_PACKAGE_ROOT, pnpmLockfileLocation: pnpmLockfileAbsolutePath, pnpmfileLocation: commonTempFolder + '/.pnpmfile.cjs', projectRoot: currentFolder, @@ -73,7 +75,7 @@ export const init = (options: { currentWorkingDirectory, appVersion, debugMode, - lockfileExplorerProjectRoot, + lockfileExplorerProjectRoot: LFX_PACKAGE_ROOT, pnpmLockfileLocation: currentFolder + '/pnpm-lock.yaml', pnpmfileLocation: currentFolder + '/.pnpmfile.cjs', projectRoot: currentFolder, diff --git a/apps/lockfile-explorer/src/utils/test/constants.test.ts b/apps/lockfile-explorer/src/utils/test/constants.test.ts new file mode 100644 index 00000000000..ab7ba1709ca --- /dev/null +++ b/apps/lockfile-explorer/src/utils/test/constants.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 { PackageJsonLookup } from '@rushstack/node-core-library/lib/PackageJsonLookup'; + +import { LFX_PACKAGE_ROOT } from '../constants'; + +describe('constants', () => { + describe('lockfileExplorerProjectRoot', () => { + it('should be a string', () => { + const actualLockfileExplorerProjectRoot: string | undefined = + PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname); + expect(LFX_PACKAGE_ROOT).toBe(actualLockfileExplorerProjectRoot); + }); + }); +}); From f3e1caddfcec3bb9a7715dffcf36983489b50a78 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sat, 18 Apr 2026 15:09:23 -0500 Subject: [PATCH 2/5] Clean up terminal actions and terminal and add a CLI help test. --- .../cli/explorer/ExplorerCommandLineParser.ts | 3 +- .../src/cli/lint/LintCommandLineParser.ts | 23 +++---- .../src/cli/lint/actions/CheckAction.ts | 5 +- .../src/cli/lint/actions/InitAction.ts | 5 +- .../src/cli/test/CommandLineHelp.test.ts | 50 ++++++++++++++ .../CommandLineHelp.test.ts.snap | 65 +++++++++++++++++++ apps/lockfile-explorer/src/start-explorer.ts | 5 +- apps/lockfile-explorer/src/start-lint.ts | 5 +- 8 files changed, 136 insertions(+), 25 deletions(-) create mode 100644 apps/lockfile-explorer/src/cli/test/CommandLineHelp.test.ts create mode 100644 apps/lockfile-explorer/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index 62060b64b62..a8940d23245 100644 --- a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -66,8 +66,7 @@ export class ExplorerCommandLineParser extends CommandLineParser { defaultValue: 'default' }); - this._terminalProvider = new ConsoleTerminalProvider(); - this.globalTerminal = new Terminal(this._terminalProvider); + this.globalTerminal = terminal; } public get isDebug(): boolean { diff --git a/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts index dfd9ae1b4e6..e7f4b8d6a33 100644 --- a/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/lint/LintCommandLineParser.ts @@ -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 { - 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)); } } diff --git a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts index 261af43cf7e..a7b7347788f 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts @@ -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, @@ -45,7 +44,7 @@ export class CheckAction extends CommandLineAction { private _checkedProjects: Set; private _docMap: Map; - public constructor(parser: LintCommandLineParser) { + public constructor(terminal: ITerminal) { super({ actionName: 'check', summary: 'Check and report dependency issues in your workspace', @@ -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(); } diff --git a/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts index c848dc47734..834f7761f55 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/InitAction.ts @@ -8,13 +8,12 @@ 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`, @@ -22,7 +21,7 @@ export class InitAction extends CommandLineAction { `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 { diff --git a/apps/lockfile-explorer/src/cli/test/CommandLineHelp.test.ts b/apps/lockfile-explorer/src/cli/test/CommandLineHelp.test.ts new file mode 100644 index 00000000000..945cb6496b2 --- /dev/null +++ b/apps/lockfile-explorer/src/cli/test/CommandLineHelp.test.ts @@ -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); + } + }); + }); +}); diff --git a/apps/lockfile-explorer/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/lockfile-explorer/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap new file mode 100644 index 00000000000..ac8ca6f0d5b --- /dev/null +++ b/apps/lockfile-explorer/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -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 + -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] ... + +Lockfile Lint applies configured policies to find and report dependency +issues in your PNPM workspace. + +Positional arguments: + + 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 +-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 []`; diff --git a/apps/lockfile-explorer/src/start-explorer.ts b/apps/lockfile-explorer/src/start-explorer.ts index 217080ff9d5..8f42f3893ce 100644 --- a/apps/lockfile-explorer/src/start-explorer.ts +++ b/apps/lockfile-explorer/src/start-explorer.ts @@ -1,8 +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, Terminal } from '@rushstack/terminal/lib/index'; + import { ExplorerCommandLineParser } from './cli/explorer/ExplorerCommandLineParser'; -const parser: ExplorerCommandLineParser = new ExplorerCommandLineParser(); +const terminal: Terminal = new Terminal(new ConsoleTerminalProvider()); +const parser: ExplorerCommandLineParser = new ExplorerCommandLineParser(terminal); 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 index f911d04dfde..29f02653d35 100644 --- a/apps/lockfile-explorer/src/start-lint.ts +++ b/apps/lockfile-explorer/src/start-lint.ts @@ -1,8 +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, Terminal } from '@rushstack/terminal/lib/index'; + import { LintCommandLineParser } from './cli/lint/LintCommandLineParser'; -const parser: LintCommandLineParser = new LintCommandLineParser(); +const terminal: Terminal = new Terminal(new ConsoleTerminalProvider()); +const parser: LintCommandLineParser = new LintCommandLineParser(terminal); parser.executeAsync().catch(console.error); // CommandLineParser.executeAsync() should never reject the promise From 20b19b92411a27b5ac80077e1cfdd192ec9a0ded Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sat, 18 Apr 2026 15:19:45 -0500 Subject: [PATCH 3/5] [rush-lib] Add PackageUpdateChecker; [lockfile-explorer] replace update-notifier - Add `PackageUpdateChecker` class to `rush-lib` as an `@internal` utility: caches latest-version results in `~/.rushstack/update-checks/`, uses global `fetch` with a 5s timeout, and supports `forceCheck`/`skip` options - Export as `_PackageUpdateChecker` from `@microsoft/rush-lib` - Replace `update-notifier` in `lockfile-explorer` with `PackageUpdateChecker`; fire the check concurrently with server setup and display the result inside `app.listen` via a standalone `printUpdateNotification` helper - Remove `update-notifier` and `@types/update-notifier` from dependencies - Remove `update-notifier` from nonbrowser-approved-packages.json - Add unit tests for `PackageUpdateChecker` (mocking `fetch` and `JsonFile`) Co-Authored-By: Claude Sonnet 4.6 --- apps/lockfile-explorer/package.json | 4 +- .../cli/explorer/ExplorerCommandLineParser.ts | 68 +++--- .../rush/nonbrowser-approved-packages.json | 4 - .../config/subspaces/default/pnpm-lock.yaml | 88 +------- .../config/subspaces/default/repo-state.json | 2 +- common/reviews/api/rush-lib.api.md | 21 ++ libraries/rush-lib/src/index.ts | 6 + .../src/utilities/PackageUpdateChecker.ts | 207 ++++++++++++++++++ .../test/PackageUpdateChecker.test.ts | 190 ++++++++++++++++ 9 files changed, 477 insertions(+), 113 deletions(-) create mode 100644 libraries/rush-lib/src/utilities/PackageUpdateChecker.ts create mode 100644 libraries/rush-lib/src/utilities/test/PackageUpdateChecker.test.ts diff --git a/apps/lockfile-explorer/package.json b/apps/lockfile-explorer/package.json index 77956e24e57..c0a75ee3984 100644 --- a/apps/lockfile-explorer/package.json +++ b/apps/lockfile-explorer/package.json @@ -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", @@ -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", diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index a8940d23245..48d5c491cce 100644 --- a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -8,16 +8,13 @@ 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'; + _PackageUpdateChecker as PackageUpdateChecker, + type _IPackageUpdateResult as IPackageUpdateResult +} from '@microsoft/rush-lib'; +import { Executable, FileSystem, type IPackageJson, JsonFile } from '@rushstack/node-core-library'; +import { type ITerminal, Colorize } from '@rushstack/terminal'; import { type CommandLineFlagParameter, CommandLineParser, @@ -36,17 +33,31 @@ 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'; 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: @@ -74,23 +85,22 @@ export class ExplorerCommandLineParser extends CommandLineParser { } protected override async onExecuteAsync(): Promise { + 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 = updateChecker.tryGetUpdateAsync(); const PORT: number = 8091; // Must not have a trailing slash @@ -118,8 +128,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; @@ -150,7 +160,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(); }); @@ -246,7 +256,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 { @@ -281,7 +293,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()); } } }); diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index de6d664412f..e507208c2f3 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -1078,10 +1078,6 @@ "name": "typescript", "allowedCategories": [ "libraries", "tests", "vscode-extensions" ] }, - { - "name": "update-notifier", - "allowedCategories": [ "libraries" ] - }, { "name": "url-loader", "allowedCategories": [ "libraries" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 165c3afa6e8..442fa952b4e 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -230,9 +230,6 @@ importers: tslib: specifier: ~2.8.1 version: 2.8.1 - update-notifier: - specifier: ~5.1.0 - version: 5.1.0 devDependencies: '@pnpm/lockfile.types': specifier: 1002.0.1 @@ -258,9 +255,6 @@ importers: '@types/semver': specifier: 7.7.1 version: 7.7.1 - '@types/update-notifier': - specifier: ~6.0.1 - version: 6.0.8 eslint: specifier: ~9.37.0 version: 9.37.0 @@ -694,7 +688,7 @@ importers: version: link:../../webpack/webpack4-module-minifier-plugin '@storybook/react': specifier: ~6.4.18 - version: 6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@2.19.0)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1) + version: 6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@0.21.3)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1) '@types/jest': specifier: 30.0.0 version: 30.0.0 @@ -764,7 +758,7 @@ importers: version: 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@storybook/addon-essentials': specifier: ~6.4.18 - version: 6.4.22(@babel/core@7.20.12)(@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@2.19.0)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1))(@types/react@17.0.74)(babel-loader@8.2.5(@babel/core@7.20.12)(webpack@4.47.0))(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)(webpack@4.47.0) + version: 6.4.22(@babel/core@7.20.12)(@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@0.21.3)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1))(@types/react@17.0.74)(babel-loader@8.2.5(@babel/core@7.20.12)(webpack@4.47.0))(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)(webpack@4.47.0) '@storybook/addon-links': specifier: ~6.4.18 version: 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -779,7 +773,7 @@ importers: version: 6.4.22 '@storybook/react': specifier: ~6.4.18 - version: 6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@2.19.0)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1) + version: 6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@0.21.3)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1) '@storybook/theming': specifier: ~6.4.18 version: 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -10116,9 +10110,6 @@ packages: peerDependencies: '@types/express': '*' - '@types/configstore@6.0.2': - resolution: {integrity: sha512-OS//b51j9uyR3zvwD04Kfs5kHpve2qalQ18JhY/ho3voGYUTPLEG90/ocfKPI48hyHH8T04f7KEEbK6Ue60oZQ==} - '@types/connect-history-api-fallback@1.5.4': resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} @@ -10421,9 +10412,6 @@ packages: '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - '@types/update-notifier@6.0.8': - resolution: {integrity: sha512-IlDFnfSVfYQD+cKIg63DEXn3RFmd7W1iYtKQsJodcHK9R1yr8aKbKaPKfBxzPpcHCq2DU8zUq4PIPmy19Thjfg==} - '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} @@ -11580,10 +11568,6 @@ packages: resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} engines: {node: '>=10'} - boxen@7.1.1: - resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} - engines: {node: '>=14.16'} - brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} @@ -11748,10 +11732,6 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - camelcase@7.0.1: - resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} - engines: {node: '>=14.16'} - caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} @@ -11788,10 +11768,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -11882,10 +11858,6 @@ packages: resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} engines: {node: '>=6'} - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} @@ -18319,10 +18291,6 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -18881,10 +18849,6 @@ packages: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} - widest-line@4.0.1: - resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} - engines: {node: '>=12'} - wildcard@2.0.1: resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} @@ -23756,7 +23720,7 @@ snapshots: dependencies: playwright: 1.56.1 - '@pmmmwh/react-refresh-webpack-plugin@0.5.17(@types/webpack@4.41.32)(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1)(webpack@4.47.0)': + '@pmmmwh/react-refresh-webpack-plugin@0.5.17(@types/webpack@4.41.32)(react-refresh@0.11.0)(type-fest@0.21.3)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1)(webpack@4.47.0)': dependencies: ansi-html: 0.0.9 core-js-pure: 3.49.0 @@ -23769,7 +23733,7 @@ snapshots: webpack: 4.47.0 optionalDependencies: '@types/webpack': 4.41.32 - type-fest: 2.19.0 + type-fest: 0.21.3 webpack-dev-server: 5.2.3(@types/webpack@4.41.32)(webpack@4.47.0) webpack-hot-middleware: 2.26.1 @@ -25404,7 +25368,7 @@ snapshots: - webpack-cli - webpack-command - '@storybook/addon-docs@6.4.22(@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@2.19.0)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1))(@types/react@17.0.74)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)(webpack@4.47.0)': + '@storybook/addon-docs@6.4.22(@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@0.21.3)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1))(@types/react@17.0.74)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)(webpack@4.47.0)': dependencies: '@babel/core': 7.20.12 '@babel/generator': 7.29.1 @@ -25453,7 +25417,7 @@ snapshots: ts-dedent: 2.2.0 util-deprecate: 1.0.2 optionalDependencies: - '@storybook/react': 6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@2.19.0)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1) + '@storybook/react': 6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@0.21.3)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) webpack: 4.47.0 @@ -25471,13 +25435,13 @@ snapshots: - webpack-cli - webpack-command - '@storybook/addon-essentials@6.4.22(@babel/core@7.20.12)(@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@2.19.0)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1))(@types/react@17.0.74)(babel-loader@8.2.5(@babel/core@7.20.12)(webpack@4.47.0))(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)(webpack@4.47.0)': + '@storybook/addon-essentials@6.4.22(@babel/core@7.20.12)(@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@0.21.3)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1))(@types/react@17.0.74)(babel-loader@8.2.5(@babel/core@7.20.12)(webpack@4.47.0))(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)(webpack@4.47.0)': dependencies: '@babel/core': 7.20.12 '@storybook/addon-actions': 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@storybook/addon-backgrounds': 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@storybook/addon-controls': 6.4.22(@types/react@17.0.74)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2) - '@storybook/addon-docs': 6.4.22(@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@2.19.0)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1))(@types/react@17.0.74)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)(webpack@4.47.0) + '@storybook/addon-docs': 6.4.22(@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@0.21.3)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1))(@types/react@17.0.74)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)(webpack@4.47.0) '@storybook/addon-measure': 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@storybook/addon-outline': 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@storybook/addon-toolbars': 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -26326,11 +26290,11 @@ snapshots: - uglify-js - webpack-cli - '@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@2.19.0)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1)': + '@storybook/react@6.4.22(@babel/core@7.20.12)(@types/node@20.17.19)(@types/react@17.0.74)(@types/webpack@4.41.32)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(type-fest@0.21.3)(typescript@5.8.2)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1)': dependencies: '@babel/preset-flow': 7.27.1(@babel/core@7.20.12) '@babel/preset-react': 7.28.5(@babel/core@7.20.12) - '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@4.41.32)(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1)(webpack@4.47.0) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(@types/webpack@4.41.32)(react-refresh@0.11.0)(type-fest@0.21.3)(webpack-dev-server@5.2.3(@types/webpack@4.41.32)(webpack@4.47.0))(webpack-hot-middleware@2.26.1)(webpack@4.47.0) '@storybook/addons': 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@storybook/core': 6.4.22(@types/react@17.0.74)(encoding@0.1.13)(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)(webpack@4.47.0) '@storybook/core-common': 6.4.22(eslint@9.37.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2) @@ -26659,8 +26623,6 @@ snapshots: dependencies: '@types/express': 4.17.21 - '@types/configstore@6.0.2': {} - '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 5.1.1 @@ -26991,11 +26953,6 @@ snapshots: '@types/unist@2.0.11': {} - '@types/update-notifier@6.0.8': - dependencies: - '@types/configstore': 6.0.2 - boxen: 7.1.1 - '@types/use-sync-external-store@0.0.6': {} '@types/vscode@1.103.0': {} @@ -28744,17 +28701,6 @@ snapshots: widest-line: 3.1.0 wrap-ansi: 7.0.0 - boxen@7.1.1: - dependencies: - ansi-align: 3.0.1 - camelcase: 7.0.1 - chalk: 5.6.2 - cli-boxes: 3.0.0 - string-width: 5.1.2 - type-fest: 2.19.0 - widest-line: 4.0.1 - wrap-ansi: 8.1.0 - brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 @@ -28996,8 +28942,6 @@ snapshots: camelcase@6.3.0: {} - camelcase@7.0.1: {} - caniuse-api@3.0.0: dependencies: browserslist: 4.28.2 @@ -29043,8 +28987,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.6.2: {} - char-regex@1.0.2: {} character-entities-legacy@1.1.4: {} @@ -29150,8 +29092,6 @@ snapshots: cli-boxes@2.2.1: {} - cli-boxes@3.0.0: {} - cli-table3@0.6.5: dependencies: string-width: 4.2.3 @@ -37480,8 +37420,6 @@ snapshots: type-fest@0.8.1: {} - type-fest@2.19.0: {} - type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -38273,10 +38211,6 @@ snapshots: dependencies: string-width: 4.2.3 - widest-line@4.0.1: - dependencies: - string-width: 5.1.2 - wildcard@2.0.1: {} word-wrap@1.2.5: {} diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index e3196053836..cb6c1388f58 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": "b649b4390090c37d5feb374b0c04bf100edc8047", + "pnpmShrinkwrapHash": "f689999859bbe91f424355ebe833b8dbf24a34aa", "preferredVersionsHash": "029c99bd6e65c5e1f25e2848340509811ff9753c" } diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 706fca89ba0..2d6e73e6cbc 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -711,6 +711,21 @@ export interface IPackageManagerOptionsJsonBase { environmentVariables?: IConfigurationEnvironment; } +// @internal +export interface _IPackageUpdateCheckerOptions { + cacheExpiryMs?: number; + currentVersion: string; + forceCheck?: boolean; + packageName: string; + skip?: boolean; +} + +// @internal +export interface _IPackageUpdateResult { + isOutdated: boolean; + latestVersion: string; +} + // @alpha export interface IPhase { allowWarningsOnSuccess: boolean; @@ -1148,6 +1163,12 @@ export abstract class PackageManagerOptionsConfigurationBase implements IPackage readonly environmentVariables?: IConfigurationEnvironment; } +// @internal +export class _PackageUpdateChecker { + constructor(options: _IPackageUpdateCheckerOptions); + tryGetUpdateAsync(): Promise<_IPackageUpdateResult | undefined>; +} + // @alpha export class PhasedCommandHooks { readonly afterExecuteOperation: AsyncSeriesHook<[ diff --git a/libraries/rush-lib/src/index.ts b/libraries/rush-lib/src/index.ts index a333473716d..6c98ff785dc 100644 --- a/libraries/rush-lib/src/index.ts +++ b/libraries/rush-lib/src/index.ts @@ -211,3 +211,9 @@ export type { IOperationBuildCacheOptions as _IOperationBuildCacheOptions, IProjectBuildCacheOptions as _IProjectBuildCacheOptions } from './logic/buildCache/OperationBuildCache'; + +export { + PackageUpdateChecker as _PackageUpdateChecker, + type IPackageUpdateCheckerOptions as _IPackageUpdateCheckerOptions, + type IPackageUpdateResult as _IPackageUpdateResult +} from './utilities/PackageUpdateChecker'; diff --git a/libraries/rush-lib/src/utilities/PackageUpdateChecker.ts b/libraries/rush-lib/src/utilities/PackageUpdateChecker.ts new file mode 100644 index 00000000000..2a969fb2ed0 --- /dev/null +++ b/libraries/rush-lib/src/utilities/PackageUpdateChecker.ts @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { homedir } from 'node:os'; + +import semver from 'semver'; + +import { type IPackageJson, JsonFile } from '@rushstack/node-core-library'; + +/** + * Options for {@link _PackageUpdateChecker}. + * + * @internal + */ +export interface IPackageUpdateCheckerOptions { + /** + * The npm package name to check for updates. + */ + packageName: string; + + /** + * The currently installed version. + */ + currentVersion: string; + + /** + * If `true`, skip the update check entirely. + * Use this to suppress checks in CI environments or non-interactive sessions. + * + * @defaultValue false + */ + skip?: boolean; + + /** + * If `true`, bypass the cache and always fetch from the registry. + * Useful in debug/verbose modes where you want an immediate, authoritative answer. + * + * @defaultValue false + */ + forceCheck?: boolean; + + /** + * How long (in milliseconds) to consider a cached registry response fresh + * before re-fetching. + * + * @defaultValue 86400000 (24 hours) + */ + cacheExpiryMs?: number; +} + +/** + * The result of an update check. + * + * @internal + */ +export interface IPackageUpdateResult { + /** + * The latest version available on the registry. + */ + latestVersion: string; + + /** + * `true` if {@link _IPackageUpdateResult.latestVersion} is strictly newer than + * the {@link _IPackageUpdateCheckerOptions.currentVersion} that was passed to the checker. + */ + isOutdated: boolean; +} + +interface IUpdateCheckCache { + checkedAt: number; + latestVersion: string; +} + +interface IUpdateCheckCacheOnDisk extends IUpdateCheckCache { + cacheVersion: typeof CACHE_VERSION; +} + +const REGISTRY_BASE_URL: 'https://registry.npmjs.org' = 'https://registry.npmjs.org'; +const FETCH_TIMEOUT_MS: 5000 = 5000; +const DEFAULT_CACHE_EXPIRY_MS: number = 24 * 60 * 60 * 1000; // 24 hours +const CACHE_VERSION: 1 = 1; +const CACHE_FOLDER: string = `${homedir()}/.rushstack/update-checks`; + +async function _tryFetchLatestVersionAsync(packageName: string): Promise { + const url: string = `${REGISTRY_BASE_URL}/${encodeURIComponent(packageName)}/latest`; + const controller: AbortController = new AbortController(); + const timeout: NodeJS.Timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response: Response = await fetch(url, { signal: controller.signal }); + if (!response.ok) { + return undefined; + } + + const { version } = (await response.json()) as IPackageJson; + return typeof version === 'string' ? version : undefined; + } catch { + // Network errors, timeouts, and parse failures are all silent. + return undefined; + } finally { + clearTimeout(timeout); + } +} + +async function _readCacheAsync(filePath: string): Promise { + try { + const data: IUpdateCheckCacheOnDisk = await JsonFile.loadAsync(filePath); + const { cacheVersion, ...rest } = data; + if (cacheVersion === CACHE_VERSION) { + return rest; + } + } catch { + // Ignore + } +} + +async function _writeCacheAsync( + filePath: string, + cache: Omit +): Promise { + try { + const cacheData: IUpdateCheckCacheOnDisk = { + cacheVersion: CACHE_VERSION, + checkedAt: Date.now(), + ...cache + }; + await JsonFile.saveAsync(cacheData, filePath, { + ensureFolderExists: true + }); + } catch { + // Cache write failures are silent — a stale or missing cache just means + // we'll re-fetch on the next invocation. + } +} + +/** + * Checks npm for a newer version of a package and caches the result locally so that + * the registry is not queried on every invocation. + * + * @internal + */ +export class PackageUpdateChecker { + private readonly _packageName: string; + private readonly _currentVersion: string; + private readonly _skip: boolean; + private readonly _forceCheck: boolean; + private readonly _cacheExpiryMs: number; + + public constructor(options: IPackageUpdateCheckerOptions) { + const { + packageName, + currentVersion, + skip = false, + forceCheck = false, + cacheExpiryMs = DEFAULT_CACHE_EXPIRY_MS + } = options; + this._packageName = packageName; + this._currentVersion = currentVersion; + this._skip = skip; + this._forceCheck = forceCheck; + this._cacheExpiryMs = cacheExpiryMs; + } + + /** + * Performs the update check and returns the result, or `undefined` if the check + * was skipped or the registry could not be reached. + */ + public async tryGetUpdateAsync(): Promise { + if (this._skip) { + return undefined; + } + + const cacheFilePath: string = this._getCacheFilePath(); + + let latestVersion: string | undefined; + if (!this._forceCheck) { + const cached: IUpdateCheckCache | undefined = await _readCacheAsync(cacheFilePath); + if (cached !== undefined) { + const { checkedAt, latestVersion: latestVersionFromCache } = cached; + const ageMs: number = Date.now() - checkedAt; + if (ageMs < this._cacheExpiryMs) { + latestVersion = latestVersionFromCache; + } + } + } + + if (latestVersion === undefined) { + // Cache is missing or stale — fetch from the registry. + latestVersion = await _tryFetchLatestVersionAsync(this._packageName); + if (latestVersion === undefined) { + return undefined; + } + + await _writeCacheAsync(cacheFilePath, { latestVersion }); + } + + return { + latestVersion, + isOutdated: semver.gt(latestVersion, this._currentVersion) + }; + } + + private _getCacheFilePath(): string { + // Replace characters that are unsafe in file names (e.g. the "/" in scoped package names). + const sanitizedName: string = this._packageName.replace(/[^a-zA-Z0-9._-]/g, '_'); + return `${CACHE_FOLDER}/${sanitizedName}.json`; + } +} diff --git a/libraries/rush-lib/src/utilities/test/PackageUpdateChecker.test.ts b/libraries/rush-lib/src/utilities/test/PackageUpdateChecker.test.ts new file mode 100644 index 00000000000..062eab6d054 --- /dev/null +++ b/libraries/rush-lib/src/utilities/test/PackageUpdateChecker.test.ts @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { JsonFile } from '@rushstack/node-core-library'; + +import { PackageUpdateChecker } from '../PackageUpdateChecker'; + +const CURRENT_VERSION: string = '1.0.0'; +const LATEST_VERSION: string = '2.0.0'; +const PACKAGE_NAME: string = '@rushstack/test-pkg'; + +function makeFetchResponse(version: string, ok: boolean = true): Response { + return { + ok, + // eslint-disable-next-line @typescript-eslint/naming-convention + json: async () => ({ version }) + } as unknown as Response; +} + +function makeCacheEntry(latestVersion: string, ageMs: number = 0): object { + return { cacheVersion: 1, checkedAt: Date.now() - ageMs, latestVersion }; +} + +describe(PackageUpdateChecker.name, () => { + let fetchSpy: jest.SpyInstance; + let loadSpy: jest.SpyInstance; + let saveSpy: jest.SpyInstance; + + beforeEach(() => { + fetchSpy = jest.spyOn(global, 'fetch' as never); + loadSpy = jest.spyOn(JsonFile, 'loadAsync'); + saveSpy = jest.spyOn(JsonFile, 'saveAsync').mockResolvedValue(false); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns undefined when skip is true', async () => { + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION, + skip: true + }); + expect(await checker.tryGetUpdateAsync()).toBeUndefined(); + expect(fetchSpy).not.toHaveBeenCalled(); + expect(loadSpy).not.toHaveBeenCalled(); + }); + + it('returns cached result without fetching when cache is fresh', async () => { + loadSpy.mockResolvedValue(makeCacheEntry(LATEST_VERSION, 1000)); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION + }); + const result = await checker.tryGetUpdateAsync(); + + expect(result).toEqual({ latestVersion: LATEST_VERSION, isOutdated: true }); + expect(fetchSpy).not.toHaveBeenCalled(); + expect(saveSpy).not.toHaveBeenCalled(); + }); + + it('fetches and writes cache when cache is stale', async () => { + const oneDayMs: number = 24 * 60 * 60 * 1000; + loadSpy.mockResolvedValue(makeCacheEntry(CURRENT_VERSION, oneDayMs + 1)); + fetchSpy.mockResolvedValue(makeFetchResponse(LATEST_VERSION)); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION + }); + const result = await checker.tryGetUpdateAsync(); + + expect(result).toEqual({ latestVersion: LATEST_VERSION, isOutdated: true }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(saveSpy).toHaveBeenCalledTimes(1); + }); + + it('fetches and writes cache when no cache exists', async () => { + loadSpy.mockRejectedValue(new Error('ENOENT')); + fetchSpy.mockResolvedValue(makeFetchResponse(LATEST_VERSION)); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION + }); + const result = await checker.tryGetUpdateAsync(); + + expect(result).toEqual({ latestVersion: LATEST_VERSION, isOutdated: true }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(saveSpy).toHaveBeenCalledTimes(1); + }); + + it('bypasses cache and fetches when forceCheck is true', async () => { + loadSpy.mockResolvedValue(makeCacheEntry(CURRENT_VERSION, 0)); + fetchSpy.mockResolvedValue(makeFetchResponse(LATEST_VERSION)); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION, + forceCheck: true + }); + const result = await checker.tryGetUpdateAsync(); + + expect(result).toEqual({ latestVersion: LATEST_VERSION, isOutdated: true }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(loadSpy).not.toHaveBeenCalled(); + }); + + it('returns undefined on network error', async () => { + loadSpy.mockRejectedValue(new Error('ENOENT')); + fetchSpy.mockRejectedValue(new Error('network error')); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION + }); + expect(await checker.tryGetUpdateAsync()).toBeUndefined(); + expect(saveSpy).not.toHaveBeenCalled(); + }); + + it('returns undefined on non-ok HTTP response', async () => { + loadSpy.mockRejectedValue(new Error('ENOENT')); + fetchSpy.mockResolvedValue(makeFetchResponse('', false)); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION + }); + expect(await checker.tryGetUpdateAsync()).toBeUndefined(); + }); + + it('sets isOutdated to false when already on latest', async () => { + loadSpy.mockResolvedValue(makeCacheEntry(CURRENT_VERSION, 1000)); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION + }); + const result = await checker.tryGetUpdateAsync(); + + expect(result).toEqual({ latestVersion: CURRENT_VERSION, isOutdated: false }); + }); + + it('ignores cache with wrong cacheVersion and re-fetches', async () => { + loadSpy.mockResolvedValue({ cacheVersion: 99, checkedAt: Date.now(), latestVersion: CURRENT_VERSION }); + fetchSpy.mockResolvedValue(makeFetchResponse(LATEST_VERSION)); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION + }); + const result = await checker.tryGetUpdateAsync(); + + expect(result).toEqual({ latestVersion: LATEST_VERSION, isOutdated: true }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it('uses custom cacheExpiryMs', async () => { + const shortExpiry: number = 5000; + loadSpy.mockResolvedValue(makeCacheEntry(LATEST_VERSION, shortExpiry + 1)); + fetchSpy.mockResolvedValue(makeFetchResponse('3.0.0')); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION, + cacheExpiryMs: shortExpiry + }); + const result = await checker.tryGetUpdateAsync(); + + // Cache was stale by 1ms under the custom expiry, so a fresh fetch should have been made. + expect(result?.latestVersion).toBe('3.0.0'); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it('sanitizes scoped package name in cache file path', async () => { + loadSpy.mockRejectedValue(new Error('ENOENT')); + fetchSpy.mockResolvedValue(makeFetchResponse(LATEST_VERSION)); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: '@scope/pkg-name', + currentVersion: CURRENT_VERSION + }); + await checker.tryGetUpdateAsync(); + + const savedPath: string = saveSpy.mock.calls[0][1] as string; + expect(savedPath).toMatch(/_scope_pkg-name\.json$/); + }); +}); From 2ae11ca0b3b76517425be20b4df0a6299e3a8dd2 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sat, 18 Apr 2026 15:40:01 -0500 Subject: [PATCH 4/5] Move PackageUpdateChecker to lockfile-explorer. --- .../cli/explorer/ExplorerCommandLineParser.ts | 5 +---- .../src/utils}/PackageUpdateChecker.ts | 5 +++++ .../utils}/test/PackageUpdateChecker.test.ts | 0 common/reviews/api/rush-lib.api.md | 21 ------------------- libraries/rush-lib/src/index.ts | 6 ------ 5 files changed, 6 insertions(+), 31 deletions(-) rename {libraries/rush-lib/src/utilities => apps/lockfile-explorer/src/utils}/PackageUpdateChecker.ts (97%) rename {libraries/rush-lib/src/utilities => apps/lockfile-explorer/src/utils}/test/PackageUpdateChecker.test.ts (100%) diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index 48d5c491cce..fab4cb5765b 100644 --- a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -9,10 +9,6 @@ import express from 'express'; import yaml from 'js-yaml'; import cors from 'cors'; -import { - _PackageUpdateChecker as PackageUpdateChecker, - type _IPackageUpdateResult as IPackageUpdateResult -} from '@microsoft/rush-lib'; import { Executable, FileSystem, type IPackageJson, JsonFile } from '@rushstack/node-core-library'; import { type ITerminal, Colorize } from '@rushstack/terminal'; import { @@ -34,6 +30,7 @@ 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'; diff --git a/libraries/rush-lib/src/utilities/PackageUpdateChecker.ts b/apps/lockfile-explorer/src/utils/PackageUpdateChecker.ts similarity index 97% rename from libraries/rush-lib/src/utilities/PackageUpdateChecker.ts rename to apps/lockfile-explorer/src/utils/PackageUpdateChecker.ts index 2a969fb2ed0..9692d4bf43a 100644 --- a/libraries/rush-lib/src/utilities/PackageUpdateChecker.ts +++ b/apps/lockfile-explorer/src/utils/PackageUpdateChecker.ts @@ -7,6 +7,11 @@ import semver from 'semver'; import { type IPackageJson, JsonFile } from '@rushstack/node-core-library'; +/** + * TODO: If we end up expecting to use this elsewhere, we should move this to + * either its own package or into `@rushstack/node-core-library`. + */ + /** * Options for {@link _PackageUpdateChecker}. * diff --git a/libraries/rush-lib/src/utilities/test/PackageUpdateChecker.test.ts b/apps/lockfile-explorer/src/utils/test/PackageUpdateChecker.test.ts similarity index 100% rename from libraries/rush-lib/src/utilities/test/PackageUpdateChecker.test.ts rename to apps/lockfile-explorer/src/utils/test/PackageUpdateChecker.test.ts diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 2d6e73e6cbc..706fca89ba0 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -711,21 +711,6 @@ export interface IPackageManagerOptionsJsonBase { environmentVariables?: IConfigurationEnvironment; } -// @internal -export interface _IPackageUpdateCheckerOptions { - cacheExpiryMs?: number; - currentVersion: string; - forceCheck?: boolean; - packageName: string; - skip?: boolean; -} - -// @internal -export interface _IPackageUpdateResult { - isOutdated: boolean; - latestVersion: string; -} - // @alpha export interface IPhase { allowWarningsOnSuccess: boolean; @@ -1163,12 +1148,6 @@ export abstract class PackageManagerOptionsConfigurationBase implements IPackage readonly environmentVariables?: IConfigurationEnvironment; } -// @internal -export class _PackageUpdateChecker { - constructor(options: _IPackageUpdateCheckerOptions); - tryGetUpdateAsync(): Promise<_IPackageUpdateResult | undefined>; -} - // @alpha export class PhasedCommandHooks { readonly afterExecuteOperation: AsyncSeriesHook<[ diff --git a/libraries/rush-lib/src/index.ts b/libraries/rush-lib/src/index.ts index 6c98ff785dc..a333473716d 100644 --- a/libraries/rush-lib/src/index.ts +++ b/libraries/rush-lib/src/index.ts @@ -211,9 +211,3 @@ export type { IOperationBuildCacheOptions as _IOperationBuildCacheOptions, IProjectBuildCacheOptions as _IProjectBuildCacheOptions } from './logic/buildCache/OperationBuildCache'; - -export { - PackageUpdateChecker as _PackageUpdateChecker, - type IPackageUpdateCheckerOptions as _IPackageUpdateCheckerOptions, - type IPackageUpdateResult as _IPackageUpdateResult -} from './utilities/PackageUpdateChecker'; From 161c5eb0b40595f06b06d64833132156eba02aa0 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sat, 18 Apr 2026 15:41:40 -0500 Subject: [PATCH 5/5] Rush change. --- .../update-notifier_2026-04-18-20-41.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/lockfile-explorer/update-notifier_2026-04-18-20-41.json diff --git a/common/changes/@rushstack/lockfile-explorer/update-notifier_2026-04-18-20-41.json b/common/changes/@rushstack/lockfile-explorer/update-notifier_2026-04-18-20-41.json new file mode 100644 index 00000000000..679f130f12e --- /dev/null +++ b/common/changes/@rushstack/lockfile-explorer/update-notifier_2026-04-18-20-41.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/lockfile-explorer", + "comment": "Replace `update-notifier` with a simpler built-in solution.", + "type": "patch" + } + ], + "packageName": "@rushstack/lockfile-explorer" +} \ No newline at end of file