diff --git a/apps/rush/src/RushFrontend.ts b/apps/rush/src/RushFrontend.ts index d598e03aae7..915115b9dbf 100644 --- a/apps/rush/src/RushFrontend.ts +++ b/apps/rush/src/RushFrontend.ts @@ -47,59 +47,59 @@ export interface IRushFrontendProcessLifecycle { } class RushFrontendReporterLifecycle { - private readonly _reporterHost: IInitializedRushReporterHost; - private readonly _processLifecycle: IRushFrontendProcessLifecycle; - private _disposeBeforeExit: (() => void) | undefined; - private readonly _disposeSignalHandlers: Array<() => void> = []; - private _closePromise: Promise | undefined; + readonly #reporterHost: IInitializedRushReporterHost; + readonly #processLifecycle: IRushFrontendProcessLifecycle; + #disposeBeforeExit: (() => void) | undefined; + readonly #disposeSignalHandlers: Array<() => void> = []; + #closePromise: Promise | undefined; public constructor( reporterHost: IInitializedRushReporterHost, processLifecycle: IRushFrontendProcessLifecycle ) { - this._reporterHost = reporterHost; - this._processLifecycle = processLifecycle; + this.#reporterHost = reporterHost; + this.#processLifecycle = processLifecycle; } public start(): void { - this._disposeBeforeExit = this._processLifecycle.registerBeforeExit(() => { + this.#disposeBeforeExit = this.#processLifecycle.registerBeforeExit(() => { void this.closeAsync().catch((error: Error) => { - this._processLifecycle.reportCloseError(error); - this._processLifecycle.setExitCode(1); + this.#processLifecycle.reportCloseError(error); + this.#processLifecycle.setExitCode(1); }); }); for (const signal of ['SIGINT', 'SIGTERM'] as const) { - this._disposeSignalHandlers.push( - this._processLifecycle.registerSignal(signal, () => { - this._disposeSignals(); - void this._closeForSignalAsync(signal); + this.#disposeSignalHandlers.push( + this.#processLifecycle.registerSignal(signal, () => { + this.#disposeSignals(); + void this.#closeForSignalAsync(signal); }) ); } } public closeAsync(timeoutMs?: number): Promise { - if (!this._closePromise) { - this._closePromise = Promise.resolve() - .then(() => this._reporterHost.closeAsync(timeoutMs)) - .finally(() => this._dispose()); + if (!this.#closePromise) { + this.#closePromise = Promise.resolve() + .then(() => this.#reporterHost.closeAsync(timeoutMs)) + .finally(() => this.#dispose()); } - return this._closePromise; + return this.#closePromise; } - private _dispose(): void { - this._disposeBeforeExit?.(); - this._disposeBeforeExit = undefined; - this._disposeSignals(); + #dispose(): void { + this.#disposeBeforeExit?.(); + this.#disposeBeforeExit = undefined; + this.#disposeSignals(); } - private _disposeSignals(): void { - for (const dispose of this._disposeSignalHandlers.splice(0)) { + #disposeSignals(): void { + for (const dispose of this.#disposeSignalHandlers.splice(0)) { dispose(); } } - private async _closeForSignalAsync(signal: RushTerminationSignal): Promise { + async #closeForSignalAsync(signal: RushTerminationSignal): Promise { const closeResult: Promise = this.closeAsync(DEFAULT_SIGNAL_FLUSH_TIMEOUT_MS).then( () => undefined, (error: Error) => error @@ -114,14 +114,14 @@ class RushFrontendReporterLifecycle { clearTimeout(timeout); } if (result === 'deadline') { - this._processLifecycle.reportCloseError( + this.#processLifecycle.reportCloseError( new Error(`Reporter close exceeded the ${DEFAULT_SIGNAL_FLUSH_TIMEOUT_MS}ms signal deadline.`) ); } else if (result) { - this._processLifecycle.reportCloseError(result); + this.#processLifecycle.reportCloseError(result); } - this._dispose(); - this._processLifecycle.terminate(signal); + this.#dispose(); + this.#processLifecycle.terminate(signal); } } diff --git a/apps/rush/src/RushReporterHost.ts b/apps/rush/src/RushReporterHost.ts index c204edeba5b..9c55622e8ad 100644 --- a/apps/rush/src/RushReporterHost.ts +++ b/apps/rush/src/RushReporterHost.ts @@ -90,42 +90,42 @@ interface IParsedReporterControls { class LogLevelReporter implements IReporter { public readonly name: string; - private readonly _reporter: IReporter; - private readonly _logLevel: ReporterLogLevel; + readonly #reporter: IReporter; + readonly #logLevel: ReporterLogLevel; public constructor(reporter: IReporter, logLevel: ReporterLogLevel) { - this._reporter = reporter; - this._logLevel = logLevel; + this.#reporter = reporter; + this.#logLevel = logLevel; this.name = reporter.name; } public initializeAsync(context: IReporterContext): Promise { - return this._reporter.initializeAsync(context); + return this.#reporter.initializeAsync(context); } public report(event: IReporterEventEnvelope): void { - if (shouldRenderAtLogLevel(this._logLevel, event)) { - this._reporter.report(event); + if (shouldRenderAtLogLevel(this.#logLevel, event)) { + this.#reporter.report(event); } } public flushAsync(): Promise { - return this._reporter.flushAsync(); + return this.#reporter.flushAsync(); } public closeAsync(): Promise { - return this._reporter.closeAsync(); + return this.#reporter.closeAsync(); } } class ExplicitOutputReporter implements IReporter { public readonly name: string; - private readonly _reporter: JsonReporter; - private readonly _filteredReporter: LogLevelReporter; - private readonly _outputPath: string; - private readonly _outputStream: IRushReporterOutputStream | undefined; - private _fileDescriptor: number | undefined; + readonly #reporter: JsonReporter; + readonly #filteredReporter: LogLevelReporter; + readonly #outputPath: string; + readonly #outputStream: IRushReporterOutputStream | undefined; + #fileDescriptor: number | undefined; public constructor( reporterName: string, @@ -134,49 +134,49 @@ class ExplicitOutputReporter implements IReporter { outputStream?: IRushReporterOutputStream ) { this.name = `${reporterName}-output`; - this._outputPath = outputPath; - this._outputStream = outputStream; - this._reporter = new JsonReporter({ + this.#outputPath = outputPath; + this.#outputStream = outputStream; + this.#reporter = new JsonReporter({ write: (text: string) => { - if (this._outputStream) { - this._outputStream.write(text); + if (this.#outputStream) { + this.#outputStream.write(text); return; } - if (this._fileDescriptor === undefined) { - throw new Error(`Reporter output ${JSON.stringify(this._outputPath)} is not initialized.`); + if (this.#fileDescriptor === undefined) { + throw new Error(`Reporter output ${JSON.stringify(this.#outputPath)} is not initialized.`); } - fs.writeSync(this._fileDescriptor, text); + fs.writeSync(this.#fileDescriptor, text); } }); - this._filteredReporter = new LogLevelReporter(this._reporter, logLevel); + this.#filteredReporter = new LogLevelReporter(this.#reporter, logLevel); } public async initializeAsync(context: IReporterContext): Promise { - if (!this._outputStream) { - await fs.promises.mkdir(path.dirname(this._outputPath), { recursive: true }); - this._fileDescriptor = fs.openSync(this._outputPath, 'w', 0o600); + if (!this.#outputStream) { + await fs.promises.mkdir(path.dirname(this.#outputPath), { recursive: true }); + this.#fileDescriptor = fs.openSync(this.#outputPath, 'w', 0o600); } - await this._filteredReporter.initializeAsync(context); + await this.#filteredReporter.initializeAsync(context); } public report(event: IReporterEventEnvelope): void { - this._filteredReporter.report(event); + this.#filteredReporter.report(event); } public async flushAsync(): Promise { - await this._filteredReporter.flushAsync(); - if (this._fileDescriptor !== undefined) { - fs.fsyncSync(this._fileDescriptor); + await this.#filteredReporter.flushAsync(); + if (this.#fileDescriptor !== undefined) { + fs.fsyncSync(this.#fileDescriptor); } } public async closeAsync(): Promise { try { - await this._filteredReporter.closeAsync(); + await this.#filteredReporter.closeAsync(); } finally { - if (this._fileDescriptor !== undefined) { - fs.closeSync(this._fileDescriptor); - this._fileDescriptor = undefined; + if (this.#fileDescriptor !== undefined) { + fs.closeSync(this.#fileDescriptor); + this.#fileDescriptor = undefined; } } } diff --git a/build-tests-samples/heft-node-jest-tutorial/src/guide/02-manual-mock/SoundPlayer.ts b/build-tests-samples/heft-node-jest-tutorial/src/guide/02-manual-mock/SoundPlayer.ts index e1459e70e6e..6816c936464 100644 --- a/build-tests-samples/heft-node-jest-tutorial/src/guide/02-manual-mock/SoundPlayer.ts +++ b/build-tests-samples/heft-node-jest-tutorial/src/guide/02-manual-mock/SoundPlayer.ts @@ -5,16 +5,16 @@ // https://jestjs.io/docs/en/es6-class-mocks export class SoundPlayer { - private _foo: string; + #foo: string; public constructor() { - this._foo = 'bar'; + this.#foo = 'bar'; } public playSoundFile(fileName: string): void { // eslint-disable-next-line no-console console.log('Playing sound file ' + fileName); // eslint-disable-next-line no-console - console.log('Foo=' + this._foo); + console.log('Foo=' + this.#foo); } } diff --git a/build-tests-samples/heft-node-jest-tutorial/src/guide/02-manual-mock/SoundPlayerConsumer.ts b/build-tests-samples/heft-node-jest-tutorial/src/guide/02-manual-mock/SoundPlayerConsumer.ts index c860b9f816a..b9224773f71 100644 --- a/build-tests-samples/heft-node-jest-tutorial/src/guide/02-manual-mock/SoundPlayerConsumer.ts +++ b/build-tests-samples/heft-node-jest-tutorial/src/guide/02-manual-mock/SoundPlayerConsumer.ts @@ -7,13 +7,13 @@ import { SoundPlayer } from './SoundPlayer'; export class SoundPlayerConsumer { - private _soundPlayer: SoundPlayer; + #soundPlayer: SoundPlayer; public constructor() { - this._soundPlayer = new SoundPlayer(); + this.#soundPlayer = new SoundPlayer(); } public playSomethingCool(): void { const coolSoundFileName: string = 'song.mp3'; - this._soundPlayer.playSoundFile(coolSoundFileName); + this.#soundPlayer.playSoundFile(coolSoundFileName); } } diff --git a/build-tests-samples/heft-node-jest-tutorial/src/guide/SoundPlayer.ts b/build-tests-samples/heft-node-jest-tutorial/src/guide/SoundPlayer.ts index e1459e70e6e..6816c936464 100644 --- a/build-tests-samples/heft-node-jest-tutorial/src/guide/SoundPlayer.ts +++ b/build-tests-samples/heft-node-jest-tutorial/src/guide/SoundPlayer.ts @@ -5,16 +5,16 @@ // https://jestjs.io/docs/en/es6-class-mocks export class SoundPlayer { - private _foo: string; + #foo: string; public constructor() { - this._foo = 'bar'; + this.#foo = 'bar'; } public playSoundFile(fileName: string): void { // eslint-disable-next-line no-console console.log('Playing sound file ' + fileName); // eslint-disable-next-line no-console - console.log('Foo=' + this._foo); + console.log('Foo=' + this.#foo); } } diff --git a/build-tests-samples/heft-node-jest-tutorial/src/guide/SoundPlayerConsumer.ts b/build-tests-samples/heft-node-jest-tutorial/src/guide/SoundPlayerConsumer.ts index c860b9f816a..b9224773f71 100644 --- a/build-tests-samples/heft-node-jest-tutorial/src/guide/SoundPlayerConsumer.ts +++ b/build-tests-samples/heft-node-jest-tutorial/src/guide/SoundPlayerConsumer.ts @@ -7,13 +7,13 @@ import { SoundPlayer } from './SoundPlayer'; export class SoundPlayerConsumer { - private _soundPlayer: SoundPlayer; + #soundPlayer: SoundPlayer; public constructor() { - this._soundPlayer = new SoundPlayer(); + this.#soundPlayer = new SoundPlayer(); } public playSomethingCool(): void { const coolSoundFileName: string = 'song.mp3'; - this._soundPlayer.playSoundFile(coolSoundFileName); + this.#soundPlayer.playSoundFile(coolSoundFileName); } } diff --git a/build-tests-samples/heft-storybook-v6-react-tutorial/src/ExampleApp.tsx b/build-tests-samples/heft-storybook-v6-react-tutorial/src/ExampleApp.tsx index d81154eaefe..b30691e2c52 100644 --- a/build-tests-samples/heft-storybook-v6-react-tutorial/src/ExampleApp.tsx +++ b/build-tests-samples/heft-storybook-v6-react-tutorial/src/ExampleApp.tsx @@ -31,7 +31,7 @@ export class ExampleApp extends React.Component { // React event handlers should be represented as fields instead of methods to ensure the "this" pointer // is bound correctly. This form does not work with virtual/override inheritance, so use regular methods // everywhere else. - private _onToggle = (sender: ToggleSwitch, args: IToggleEventArgs): void => { + protected _onToggle = (sender: ToggleSwitch, args: IToggleEventArgs): void => { // eslint-disable-next-line no-console console.log('Toggle switch changed: ' + args.sliderPosition); }; diff --git a/build-tests-samples/heft-storybook-v6-react-tutorial/src/ToggleSwitch.tsx b/build-tests-samples/heft-storybook-v6-react-tutorial/src/ToggleSwitch.tsx index 79e43aad327..88266fd6346 100644 --- a/build-tests-samples/heft-storybook-v6-react-tutorial/src/ToggleSwitch.tsx +++ b/build-tests-samples/heft-storybook-v6-react-tutorial/src/ToggleSwitch.tsx @@ -91,7 +91,7 @@ export class ToggleSwitch extends React.Component { + protected _onClickSlider = (event: React.MouseEvent): void => { if (this.state.sliderPosition === ToggleSwitchPosition.Left) { this.setState({ sliderPosition: ToggleSwitchPosition.Right }); } else { diff --git a/build-tests-samples/heft-storybook-v9-react-tutorial/src/ExampleApp.tsx b/build-tests-samples/heft-storybook-v9-react-tutorial/src/ExampleApp.tsx index d81154eaefe..310c1686c31 100644 --- a/build-tests-samples/heft-storybook-v9-react-tutorial/src/ExampleApp.tsx +++ b/build-tests-samples/heft-storybook-v9-react-tutorial/src/ExampleApp.tsx @@ -22,7 +22,7 @@ export class ExampleApp extends React.Component {

Hello, world!

Here is an example control: - +
); @@ -31,7 +31,7 @@ export class ExampleApp extends React.Component { // React event handlers should be represented as fields instead of methods to ensure the "this" pointer // is bound correctly. This form does not work with virtual/override inheritance, so use regular methods // everywhere else. - private _onToggle = (sender: ToggleSwitch, args: IToggleEventArgs): void => { + #onToggle = (sender: ToggleSwitch, args: IToggleEventArgs): void => { // eslint-disable-next-line no-console console.log('Toggle switch changed: ' + args.sliderPosition); }; diff --git a/build-tests-samples/heft-storybook-v9-react-tutorial/src/ToggleSwitch.tsx b/build-tests-samples/heft-storybook-v9-react-tutorial/src/ToggleSwitch.tsx index 79e43aad327..2e745043900 100644 --- a/build-tests-samples/heft-storybook-v9-react-tutorial/src/ToggleSwitch.tsx +++ b/build-tests-samples/heft-storybook-v9-react-tutorial/src/ToggleSwitch.tsx @@ -82,7 +82,7 @@ export class ToggleSwitch extends React.Component +
); @@ -91,7 +91,7 @@ export class ToggleSwitch extends React.Component { + #onClickSlider = (event: React.MouseEvent): void => { if (this.state.sliderPosition === ToggleSwitchPosition.Left) { this.setState({ sliderPosition: ToggleSwitchPosition.Right }); } else { diff --git a/build-tests-samples/heft-web-rig-app-tutorial/src/ExampleApp.tsx b/build-tests-samples/heft-web-rig-app-tutorial/src/ExampleApp.tsx index 3ef0dfdafab..8bb602a0ca0 100644 --- a/build-tests-samples/heft-web-rig-app-tutorial/src/ExampleApp.tsx +++ b/build-tests-samples/heft-web-rig-app-tutorial/src/ExampleApp.tsx @@ -34,7 +34,7 @@ export class ExampleApp extends React.Component { // React event handlers should be represented as fields instead of methods to ensure the "this" pointer // is bound correctly. This form does not work with virtual/override inheritance, so use regular methods // everywhere else. - private _onToggle = (sender: ToggleSwitch, args: IToggleEventArgs): void => { + protected _onToggle = (sender: ToggleSwitch, args: IToggleEventArgs): void => { // eslint-disable-next-line no-console console.log('Toggle switch changed: ' + args.sliderPosition); }; diff --git a/build-tests-samples/heft-web-rig-library-tutorial/src/ToggleSwitch.tsx b/build-tests-samples/heft-web-rig-library-tutorial/src/ToggleSwitch.tsx index 3d2488fd556..370a123c614 100644 --- a/build-tests-samples/heft-web-rig-library-tutorial/src/ToggleSwitch.tsx +++ b/build-tests-samples/heft-web-rig-library-tutorial/src/ToggleSwitch.tsx @@ -98,7 +98,7 @@ export class ToggleSwitch extends React.Component { + protected _onClickSlider = (event: React.MouseEvent): void => { if (this.state.sliderPosition === ToggleSwitchPosition.Left) { this.setState({ sliderPosition: ToggleSwitchPosition.Right }); } else { diff --git a/build-tests-samples/heft-webpack-basic-tutorial/src/ExampleApp.tsx b/build-tests-samples/heft-webpack-basic-tutorial/src/ExampleApp.tsx index d81154eaefe..b30691e2c52 100644 --- a/build-tests-samples/heft-webpack-basic-tutorial/src/ExampleApp.tsx +++ b/build-tests-samples/heft-webpack-basic-tutorial/src/ExampleApp.tsx @@ -31,7 +31,7 @@ export class ExampleApp extends React.Component { // React event handlers should be represented as fields instead of methods to ensure the "this" pointer // is bound correctly. This form does not work with virtual/override inheritance, so use regular methods // everywhere else. - private _onToggle = (sender: ToggleSwitch, args: IToggleEventArgs): void => { + protected _onToggle = (sender: ToggleSwitch, args: IToggleEventArgs): void => { // eslint-disable-next-line no-console console.log('Toggle switch changed: ' + args.sliderPosition); }; diff --git a/build-tests-samples/heft-webpack-basic-tutorial/src/ToggleSwitch.tsx b/build-tests-samples/heft-webpack-basic-tutorial/src/ToggleSwitch.tsx index 79e43aad327..88266fd6346 100644 --- a/build-tests-samples/heft-webpack-basic-tutorial/src/ToggleSwitch.tsx +++ b/build-tests-samples/heft-webpack-basic-tutorial/src/ToggleSwitch.tsx @@ -91,7 +91,7 @@ export class ToggleSwitch extends React.Component { + protected _onClickSlider = (event: React.MouseEvent): void => { if (this.state.sliderPosition === ToggleSwitchPosition.Left) { this.setState({ sliderPosition: ToggleSwitchPosition.Right }); } else { diff --git a/build-tests-samples/packlets-tutorial/src/packlets/reports/MainReport.ts b/build-tests-samples/packlets-tutorial/src/packlets/reports/MainReport.ts index 2fb96438317..ce716322c66 100644 --- a/build-tests-samples/packlets-tutorial/src/packlets/reports/MainReport.ts +++ b/build-tests-samples/packlets-tutorial/src/packlets/reports/MainReport.ts @@ -5,11 +5,11 @@ import { DataModel } from '../data-model'; import { Logger, MessageType } from '../logging'; export class MainReport { - private readonly _logger: Logger; + readonly #logger: Logger; public constructor(logger: Logger) { - this._logger = logger; - this._logger.log(MessageType.Info, 'Constructing MainReport'); + this.#logger = logger; + this.#logger.log(MessageType.Info, 'Constructing MainReport'); } public showReport(dataModel: DataModel): void { diff --git a/build-tests/eslint-8-test/src/index.ts b/build-tests/eslint-8-test/src/index.ts index 428f8caba4f..2ae11b9182c 100644 --- a/build-tests/eslint-8-test/src/index.ts +++ b/build-tests/eslint-8-test/src/index.ts @@ -2,6 +2,6 @@ // See LICENSE in the project root for license information. export class Foo { - private _bar: string = 'bar'; - public baz: string = this._bar; + #bar: string = 'bar'; + public baz: string = this.#bar; } diff --git a/build-tests/eslint-9-test/src/__snapshots__/sarif.test.ts.snap b/build-tests/eslint-9-test/src/__snapshots__/sarif.test.ts.snap index 0ddfa4d6a6f..e765ef493d8 100644 --- a/build-tests/eslint-9-test/src/__snapshots__/sarif.test.ts.snap +++ b/build-tests/eslint-9-test/src/__snapshots__/sarif.test.ts.snap @@ -28,7 +28,7 @@ Object { "uri": "src/index.ts", }, "region": Object { - "endColumn": 24, + "endColumn": 16, "endLine": 6, "startColumn": 3, "startLine": 6, @@ -37,7 +37,7 @@ Object { }, ], "message": Object { - "text": "Expected _bar to have a type annotation.", + "text": "Expected a type annotation.", }, "ruleId": "@typescript-eslint/typedef", "ruleIndex": 0, diff --git a/build-tests/eslint-9-test/src/index.ts b/build-tests/eslint-9-test/src/index.ts index 549373093be..92ad52c9244 100644 --- a/build-tests/eslint-9-test/src/index.ts +++ b/build-tests/eslint-9-test/src/index.ts @@ -3,8 +3,8 @@ export class Foo { // eslint-disable-next-line @typescript-eslint/typedef - private _bar = 'bar'; - public baz: string = this._bar; + #bar = 'bar'; + public baz: string = this.#bar; } export const Bad_Name: string = '37'; diff --git a/build-tests/heft-example-plugin-01/src/index.ts b/build-tests/heft-example-plugin-01/src/index.ts index 8bc385908fa..57e0cbe99e7 100644 --- a/build-tests/heft-example-plugin-01/src/index.ts +++ b/build-tests/heft-example-plugin-01/src/index.ts @@ -17,12 +17,12 @@ export interface IExamplePlugin01Accessor { export const PLUGIN_NAME: 'example-plugin-01' = 'example-plugin-01'; export default class ExamplePlugin01 implements IHeftTaskPlugin { - private _accessor: IExamplePlugin01Accessor = { + #accessor: IExamplePlugin01Accessor = { exampleHook: new SyncHook() }; public get accessor(): IExamplePlugin01Accessor { - return this._accessor; + return this.#accessor; } public apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration): void { diff --git a/build-tests/heft-fastify-test/src/start.ts b/build-tests/heft-fastify-test/src/start.ts index 99497d954e0..faaa0ac188b 100644 --- a/build-tests/heft-fastify-test/src/start.ts +++ b/build-tests/heft-fastify-test/src/start.ts @@ -31,7 +31,7 @@ class MyApp { }); } - private async _startAsync(): Promise { + async #startAsync(): Promise { this.server.get('/', async (request, reply) => { return { hello: 'world' }; }); @@ -42,7 +42,7 @@ class MyApp { } public start(): void { - this._startAsync().catch((error) => { + this.#startAsync().catch((error) => { process.exitCode = 1; this.server.log.error(error); diff --git a/build-tests/rush-package-manager-integration-test/src/TestHelper.ts b/build-tests/rush-package-manager-integration-test/src/TestHelper.ts index 2ef48061152..4c669e5fc7c 100644 --- a/build-tests/rush-package-manager-integration-test/src/TestHelper.ts +++ b/build-tests/rush-package-manager-integration-test/src/TestHelper.ts @@ -11,24 +11,24 @@ import type { ITerminal } from '@rushstack/terminal'; * Helper class for running integration tests with Rush package managers */ export class TestHelper { - private readonly _rushBinPath: string; - private readonly _terminal: ITerminal; + readonly #rushBinPath: string; + readonly #terminal: ITerminal; public constructor(terminal: ITerminal) { - this._terminal = terminal; + this.#terminal = terminal; // Resolve rush bin path from @microsoft/rush dependency - this._rushBinPath = require.resolve('@microsoft/rush/lib/start-dev'); + this.#rushBinPath = require.resolve('@microsoft/rush/lib/start-dev'); } /** * Execute a Rush command using the locally-built Rush */ public async executeRushAsync(args: string[], workingDirectory: string): Promise { - this._terminal.writeLine(`Executing: ${process.argv0} ${this._rushBinPath} ${args.join(' ')}`); + this.#terminal.writeLine(`Executing: ${process.argv0} ${this.#rushBinPath} ${args.join(' ')}`); const childProcess: child_process.ChildProcess = Executable.spawn( process.argv0, - [this._rushBinPath, ...args], + [this.#rushBinPath, ...args], { currentWorkingDirectory: workingDirectory, stdio: 'inherit' @@ -49,15 +49,15 @@ export class TestHelper { packageManagerVersion: string ): Promise { // Clean up previous test run and create empty test repo directory - this._terminal.writeLine(`Creating test repository at ${testRepoPath}...`); + this.#terminal.writeLine(`Creating test repository at ${testRepoPath}...`); await FileSystem.ensureEmptyFolderAsync(testRepoPath); // Initialize Rush repo - this._terminal.writeLine('Initializing Rush repo...'); + this.#terminal.writeLine('Initializing Rush repo...'); await this.executeRushAsync(['init'], testRepoPath); // Configure rush.json for the specified package manager - this._terminal.writeLine(`Configuring rush.json for ${packageManagerType} mode...`); + this.#terminal.writeLine(`Configuring rush.json for ${packageManagerType} mode...`); const rushJsonPath: string = path.join(testRepoPath, 'rush.json'); const rushJson: JsonObject = await JsonFile.loadAsync(rushJsonPath); @@ -120,14 +120,14 @@ export class TestHelper { * Verify that temp project tarballs were created */ public async verifyTempTarballsAsync(testRepoPath: string, projectNames: string[]): Promise { - this._terminal.writeLine('\nVerifying temp project tarballs were created...'); + this.#terminal.writeLine('\nVerifying temp project tarballs were created...'); for (const projectName of projectNames) { const tarballPath: string = path.join(testRepoPath, 'common/temp/projects', `${projectName}.tgz`); if (!(await FileSystem.existsAsync(tarballPath))) { throw new Error(`ERROR: ${projectName}.tgz was not created!`); } } - this._terminal.writeLine('✓ Temp project tarballs created successfully'); + this.#terminal.writeLine('✓ Temp project tarballs created successfully'); } /** @@ -138,7 +138,7 @@ export class TestHelper { projectName: string, expectedDependencies: string[] ): Promise { - this._terminal.writeLine('\nVerifying node_modules structure...'); + this.#terminal.writeLine('\nVerifying node_modules structure...'); const projectPath: string = path.join(testRepoPath, 'projects', projectName); const projectNodeModules: string = path.join(projectPath, 'node_modules'); @@ -161,28 +161,28 @@ export class TestHelper { } } } - this._terminal.writeLine('✓ Dependencies installed correctly'); + this.#terminal.writeLine('✓ Dependencies installed correctly'); } /** * Verify that build outputs were created */ public async verifyBuildOutputsAsync(testRepoPath: string, projectNames: string[]): Promise { - this._terminal.writeLine('\nVerifying build outputs...'); + this.#terminal.writeLine('\nVerifying build outputs...'); for (const projectName of projectNames) { const outputPath: string = path.join(testRepoPath, 'projects', projectName, 'lib/index.js'); if (!(await FileSystem.existsAsync(outputPath))) { throw new Error(`ERROR: ${projectName} build output not found!`); } } - this._terminal.writeLine('✓ Build completed successfully'); + this.#terminal.writeLine('✓ Build completed successfully'); } /** * Test that the built code executes correctly */ public async testBuiltCodeAsync(testRepoPath: string, projectName: string): Promise { - this._terminal.writeLine('\nTesting built code...'); + this.#terminal.writeLine('\nTesting built code...'); const projectLib: string = path.join(testRepoPath, 'projects', projectName, 'lib/index.js'); // Use forward slashes for require() path on all platforms @@ -200,6 +200,6 @@ export class TestHelper { if (!result.includes('Using: Hello from A')) { throw new Error('ERROR: Built code did not execute as expected!'); } - this._terminal.writeLine('✓ Built code executes correctly'); + this.#terminal.writeLine('✓ Built code executes correctly'); } } diff --git a/build-tests/rushd-wire-e2e-test/src/test/TestWritable.ts b/build-tests/rushd-wire-e2e-test/src/test/TestWritable.ts index e3ae58c602f..e6a59e71533 100644 --- a/build-tests/rushd-wire-e2e-test/src/test/TestWritable.ts +++ b/build-tests/rushd-wire-e2e-test/src/test/TestWritable.ts @@ -4,7 +4,6 @@ import type { DaemonRenderStream, IDaemonRendererTerminal } from '@rushstack/rush-terminal-renderer'; import { type ITerminalChunk, TerminalChunkKind, TerminalWritable } from '@rushstack/terminal'; - const TEST_COLUMNS: number = 80; /** A `TerminalWritable` collecting chunk text per stream (engine side). */ @@ -55,18 +54,18 @@ function collectByKind(chunks: readonly ITerminalChunk[], kind: TerminalChunkKin export class CollectingTerminal implements IDaemonRendererTerminal { public readonly columns: number = TEST_COLUMNS; public readonly isTTY: boolean = false; - private readonly _writes: [DaemonRenderStream, string][] = []; + readonly #writes: [DaemonRenderStream, string][] = []; public write(text: string, stream: DaemonRenderStream): void { - this._writes.push([stream, text]); + this.#writes.push([stream, text]); } public get stdout(): string { - return collectWrites(this._writes, 'stdout'); + return collectWrites(this.#writes, 'stdout'); } public get stderr(): string { - return collectWrites(this._writes, 'stderr'); + return collectWrites(this.#writes, 'stderr'); } } diff --git a/build-tests/rushd-wire-e2e-test/src/test/WireAdapter.ts b/build-tests/rushd-wire-e2e-test/src/test/WireAdapter.ts index bb6eb46f1cc..0b9628701d9 100644 --- a/build-tests/rushd-wire-e2e-test/src/test/WireAdapter.ts +++ b/build-tests/rushd-wire-e2e-test/src/test/WireAdapter.ts @@ -16,7 +16,6 @@ import type { DaemonEventType, IDaemonFrame } from '@rushstack/rush-daemon-proto import { TerminalChunkKind } from '@rushstack/terminal'; import type { ITerminalChunk } from '@rushstack/terminal'; - import { buildWireEnvelope } from './WireEnvelope'; import type { IWireEnvelopeOptions } from './WireEnvelope'; @@ -30,17 +29,14 @@ function toActivityStream(options?: { stderr?: boolean }): 'stdout' | 'stderr' { /** Converts engine dual-emit callbacks into an ordered wire frame stream. */ export class WireAdapter implements IOperationGraphEventSink { public readonly frames: IDaemonFrame[] = []; - private _sequence: number = FIRST_SEQUENCE; + #sequence: number = FIRST_SEQUENCE; public onOperationRegistered(operationId: string, silent: boolean): void { - this._pushEvent('operationRegistered', { operationId, silent }); + this.#pushEvent('operationRegistered', { operationId, silent }); } - public onOperationStatusChanged( - result: IOperationExecutionResult, - previousStatus: OperationStatus - ): void { - this._pushEvent('operationStatusChanged', { + public onOperationStatusChanged(result: IOperationExecutionResult, previousStatus: OperationStatus): void { + this.#pushEvent('operationStatusChanged', { operationId: result.operation.name, status: result.status, previousStatus @@ -48,7 +44,7 @@ export class WireAdapter implements IOperationGraphEventSink { } public onOperationHeader(operationId: string, completed: number, total: number): void { - this._pushEvent('extension', { + this.#pushEvent('extension', { name: RUSHD_OPERATION_HEADER, data: { operationId, completedOperations: completed, totalOperations: total } }); @@ -67,33 +63,29 @@ export class WireAdapter implements IOperationGraphEventSink { const stream: 'stdout' | 'stderr' = toActivityStream(options); const operationId: string | undefined = options?.operationId; if (operationId === undefined) { - this._pushEvent('activityChanged', { text, stream }); + this.#pushEvent('activityChanged', { text, stream }); return; } // Operation-scoped status lines are part of the operation's output block: // scope the event and mark it required so it is never verbosity-filtered. - this._pushEvent( - 'activityChanged', - { text, stream }, - { scope: { operationId }, required: true } - ); + this.#pushEvent('activityChanged', { text, stream }, { scope: { operationId }, required: true }); } public onOperationStreamClosed(operationId: string): void { - this._pushEvent('extension', { + this.#pushEvent('extension', { name: RUSHD_OPERATION_STREAM_CLOSED, data: { operationId } }); } - private _pushEvent(type: DaemonEventType, payload: unknown, options?: IWireEnvelopeOptions): void { + #pushEvent(type: DaemonEventType, payload: unknown, options?: IWireEnvelopeOptions): void { const envelope: ReturnType = buildWireEnvelope( type, payload, - this._sequence, + this.#sequence, options ); - this._sequence += 1; + this.#sequence += 1; this.frames.push({ kind: DaemonFrameType.event, payload: encodeDaemonEventFrame(envelope) }); } } diff --git a/common/changes/@microsoft/rush/remaining-native-private-members_2026-09-10-13-36.json b/common/changes/@microsoft/rush/remaining-native-private-members_2026-09-10-13-36.json new file mode 100644 index 00000000000..65eecb7a22b --- /dev/null +++ b/common/changes/@microsoft/rush/remaining-native-private-members_2026-09-10-13-36.json @@ -0,0 +1,39 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Use ECMAScript private syntax for remaining eligible internal class members.", + "type": "none" + }, + { + "packageName": "@rushstack/heft-json-schema-typings-plugin", + "comment": "Use ECMAScript private syntax for internal class members.", + "type": "none" + }, + { + "packageName": "@rushstack/heft-localization-typings-plugin", + "comment": "Use ECMAScript private syntax for internal class members.", + "type": "none" + }, + { + "packageName": "@rushstack/heft-storybook-plugin", + "comment": "Use ECMAScript private syntax for internal class members.", + "type": "none" + }, + { + "packageName": "@rushstack/rush-mcp-docs-plugin", + "comment": "Use ECMAScript private syntax for internal class members.", + "type": "none" + }, + { + "packageName": "@rushstack/rush-reporter", + "comment": "Use ECMAScript private syntax for internal class members.", + "type": "none" + }, + { + "packageName": "@rushstack/rush-terminal-renderer", + "comment": "Use ECMAScript private syntax for internal class members.", + "type": "none" + } + ] +} diff --git a/heft-plugins/heft-json-schema-typings-plugin/src/JsonSchemaTypingsPlugin.ts b/heft-plugins/heft-json-schema-typings-plugin/src/JsonSchemaTypingsPlugin.ts index 877cd041b26..978bee68689 100644 --- a/heft-plugins/heft-json-schema-typings-plugin/src/JsonSchemaTypingsPlugin.ts +++ b/heft-plugins/heft-json-schema-typings-plugin/src/JsonSchemaTypingsPlugin.ts @@ -53,18 +53,18 @@ export default class JsonSchemaTypingsPlugin implements IHeftTaskPlugin { - await this._runTypingsGeneratorAsync(typingsGenerator, terminal, undefined); + await this.#runTypingsGeneratorAsync(typingsGenerator, terminal, undefined); }); runIncremental.tapPromise( PLUGIN_NAME, async (runIncrementalOptions: IHeftTaskRunIncrementalHookOptions) => { - await this._runTypingsGeneratorAsync(typingsGenerator, terminal, runIncrementalOptions); + await this.#runTypingsGeneratorAsync(typingsGenerator, terminal, runIncrementalOptions); } ); } - private async _runTypingsGeneratorAsync( + async #runTypingsGeneratorAsync( typingsGenerator: JsonSchemaTypingsGenerator, terminal: ITerminal, runIncrementalOptions: IHeftTaskRunIncrementalHookOptions | undefined diff --git a/heft-plugins/heft-localization-typings-plugin/src/LocalizationTypingsPlugin.ts b/heft-plugins/heft-localization-typings-plugin/src/LocalizationTypingsPlugin.ts index c93d7afca5c..2e319d2e9a3 100644 --- a/heft-plugins/heft-localization-typings-plugin/src/LocalizationTypingsPlugin.ts +++ b/heft-plugins/heft-localization-typings-plugin/src/LocalizationTypingsPlugin.ts @@ -102,18 +102,18 @@ export default class LocalizationTypingsPlugin implements IHeftTaskPlugin { - await this._runLocalizationTypingsGeneratorAsync(typingsGenerator, logger, undefined); + await this.#runLocalizationTypingsGeneratorAsync(typingsGenerator, logger, undefined); }); taskSession.hooks.runIncremental.tapPromise( PLUGIN_NAME, async (runIncrementalOptions: IHeftTaskRunIncrementalHookOptions) => { - await this._runLocalizationTypingsGeneratorAsync(typingsGenerator, logger, runIncrementalOptions); + await this.#runLocalizationTypingsGeneratorAsync(typingsGenerator, logger, runIncrementalOptions); } ); } - private async _runLocalizationTypingsGeneratorAsync( + async #runLocalizationTypingsGeneratorAsync( typingsGenerator: TypingsGenerator, { terminal }: IScopedLogger, runIncrementalOptions: IHeftTaskRunIncrementalHookOptions | undefined diff --git a/heft-plugins/heft-storybook-plugin/src/StorybookPlugin.ts b/heft-plugins/heft-storybook-plugin/src/StorybookPlugin.ts index 56d6476992c..a32a9133ac5 100644 --- a/heft-plugins/heft-storybook-plugin/src/StorybookPlugin.ts +++ b/heft-plugins/heft-storybook-plugin/src/StorybookPlugin.ts @@ -309,7 +309,7 @@ export default class StorybookPlugin implements IHeftTaskPlugin { - const runStorybookOptions: IRunStorybookOptions = await this._prepareStorybookAsync({ + const runStorybookOptions: IRunStorybookOptions = await this.#prepareStorybookAsync({ logger, taskSession, heftConfiguration, @@ -320,12 +320,12 @@ export default class StorybookPlugin implements IHeftTaskPlugin { + async #prepareStorybookAsync(options: IPrepareStorybookOptions): Promise { const { logger, taskSession, @@ -334,7 +334,7 @@ export default class StorybookPlugin implements IHeftTaskPlugin { @@ -481,7 +481,7 @@ export default class StorybookPlugin implements IHeftTaskPlugin string; - private _usedBytes: number; - private _nextSequence: number; - private _nextEventId: number; - private _truncated: boolean; - private _failed: boolean; - private _droppedReplaceable: number; - private _droppedOther: number; - private _droppedRequired: number; + readonly #entries: IBufferEntry[]; + readonly #maxBytes: number; + readonly #entryByteLimit: number; + readonly #sessionId: string; + readonly #source: IBootstrapEventSource; + readonly #now: () => string; + #usedBytes: number; + #nextSequence: number; + #nextEventId: number; + #truncated: boolean; + #failed: boolean; + #droppedReplaceable: number; + #droppedOther: number; + #droppedRequired: number; public constructor(options: IBootstrapEventBufferOptions) { - this._entries = []; - this._maxBytes = options.maxBytes ?? BOOTSTRAP_BUFFER_MAX_BYTES; - this._entryByteLimit = this._maxBytes - TRUNCATION_NOTICE_RESERVE_BYTES; - if (this._entryByteLimit <= 0) { + this.#entries = []; + this.#maxBytes = options.maxBytes ?? BOOTSTRAP_BUFFER_MAX_BYTES; + this.#entryByteLimit = this.#maxBytes - TRUNCATION_NOTICE_RESERVE_BYTES; + if (this.#entryByteLimit <= 0) { throw new RangeError(`maxBytes must be greater than ${TRUNCATION_NOTICE_RESERVE_BYTES}.`); } - this._sessionId = options.sessionId; - this._source = options.source; - this._now = options.now ?? (() => new Date().toISOString()); - this._usedBytes = 0; - this._nextSequence = 1; - this._nextEventId = 1; - this._truncated = false; - this._failed = false; - this._droppedReplaceable = 0; - this._droppedOther = 0; - this._droppedRequired = 0; + this.#sessionId = options.sessionId; + this.#source = options.source; + this.#now = options.now ?? (() => new Date().toISOString()); + this.#usedBytes = 0; + this.#nextSequence = 1; + this.#nextEventId = 1; + this.#truncated = false; + this.#failed = false; + this.#droppedReplaceable = 0; + this.#droppedOther = 0; + this.#droppedRequired = 0; } /** * Whether a required or diagnostic event could not be preserved. */ public get failed(): boolean { - return this._failed; + return this.#failed; } /** @@ -178,11 +178,11 @@ export class BootstrapEventBuffer { */ public get truncation(): IBootstrapTruncation { return { - truncated: this._truncated, - failed: this._failed, - droppedReplaceable: this._droppedReplaceable, - droppedOther: this._droppedOther, - droppedRequired: this._droppedRequired + truncated: this.#truncated, + failed: this.#failed, + droppedReplaceable: this.#droppedReplaceable, + droppedOther: this.#droppedOther, + droppedRequired: this.#droppedRequired }; } @@ -190,14 +190,14 @@ export class BootstrapEventBuffer { * Encodes and buffers an event, returning its assigned event id. */ public emit(input: IBootstrapEventInput): string { - const eventId: string = `boot_${this._nextEventId++}`; + const eventId: string = `boot_${this.#nextEventId++}`; const required: boolean = input.type !== 'activityChanged'; const line: string = encodeBootstrapEnvelope({ eventId, - sessionId: this._sessionId, - sequence: this._nextSequence++, - timestamp: this._now(), - source: this._source, + sessionId: this.#sessionId, + sequence: this.#nextSequence++, + timestamp: this.#now(), + source: this.#source, privacy: input.privacy ?? 'public', required, type: input.type, @@ -207,26 +207,26 @@ export class BootstrapEventBuffer { const mustPreserve: boolean = required; const replaceable: boolean = input.type === 'activityChanged'; - if (this._usedBytes + bytes <= this._entryByteLimit) { - this._entries.push({ line, bytes, mustPreserve, replaceable }); - this._usedBytes += bytes; + if (this.#usedBytes + bytes <= this.#entryByteLimit) { + this.#entries.push({ line, bytes, mustPreserve, replaceable }); + this.#usedBytes += bytes; return eventId; } - this._truncated = true; + this.#truncated = true; if (mustPreserve) { - this._evictToFit(bytes); - if (this._usedBytes + bytes <= this._entryByteLimit) { - this._entries.push({ line, bytes, mustPreserve, replaceable }); - this._usedBytes += bytes; + this.#evictToFit(bytes); + if (this.#usedBytes + bytes <= this.#entryByteLimit) { + this.#entries.push({ line, bytes, mustPreserve, replaceable }); + this.#usedBytes += bytes; } else { - this._failed = true; - this._droppedRequired++; + this.#failed = true; + this.#droppedRequired++; } } else if (replaceable) { - this._droppedReplaceable++; + this.#droppedReplaceable++; } else { - this._droppedOther++; + this.#droppedOther++; } return eventId; } @@ -253,23 +253,23 @@ export class BootstrapEventBuffer { * extension event when any events were lost. */ public serialize(): string { - const lines: string[] = this._entries.map((entry: IBufferEntry) => entry.line); - if (this._truncated) { + const lines: string[] = this.#entries.map((entry: IBufferEntry) => entry.line); + if (this.#truncated) { const noticeLine: string = encodeBootstrapEnvelope({ eventId: 'boot_bufferTruncated', - sessionId: this._sessionId, - sequence: this._nextSequence++, - timestamp: this._now(), - source: this._source, + sessionId: this.#sessionId, + sequence: this.#nextSequence++, + timestamp: this.#now(), + source: this.#source, privacy: 'public', required: true, type: 'extension', payload: { name: BOOTSTRAP_BUFFER_TRUNCATED_EXTENSION_NAME, - droppedReplaceable: this._droppedReplaceable, - droppedOther: this._droppedOther, - droppedRequired: this._droppedRequired, - failed: this._failed + droppedReplaceable: this.#droppedReplaceable, + droppedOther: this.#droppedOther, + droppedRequired: this.#droppedRequired, + failed: this.#failed } }); const noticeBytes: number = Buffer.byteLength(noticeLine, 'utf8') + 1; @@ -281,20 +281,20 @@ export class BootstrapEventBuffer { return lines.length > 0 ? `${lines.join('\n')}\n` : ''; } - private _evictToFit(requiredBytes: number): void { + #evictToFit(requiredBytes: number): void { let index: number = 0; - while (this._usedBytes + requiredBytes > this._entryByteLimit && index < this._entries.length) { - const entry: IBufferEntry = this._entries[index]; + while (this.#usedBytes + requiredBytes > this.#entryByteLimit && index < this.#entries.length) { + const entry: IBufferEntry = this.#entries[index]; if (entry.mustPreserve) { index++; continue; } - this._entries.splice(index, 1); - this._usedBytes -= entry.bytes; + this.#entries.splice(index, 1); + this.#usedBytes -= entry.bytes; if (entry.replaceable) { - this._droppedReplaceable++; + this.#droppedReplaceable++; } else { - this._droppedOther++; + this.#droppedOther++; } } } diff --git a/libraries/reporter/src/compat/LegacyErrorBridge.ts b/libraries/reporter/src/compat/LegacyErrorBridge.ts index 5bc6fb8d7dd..53c75c8ff4c 100644 --- a/libraries/reporter/src/compat/LegacyErrorBridge.ts +++ b/libraries/reporter/src/compat/LegacyErrorBridge.ts @@ -72,13 +72,13 @@ export function isAlreadyReportedSentinel(error: unknown): boolean { * @beta */ export class LegacyErrorBridge { - private readonly _emittedDiagnosticIds: Set = new Set(); + readonly #emittedDiagnosticIds: Set = new Set(); /** * Records that a diagnostic id has been emitted. */ public recordEmittedDiagnostic(diagnosticId: string): void { - this._emittedDiagnosticIds.add(diagnosticId); + this.#emittedDiagnosticIds.add(diagnosticId); } /** @@ -88,7 +88,7 @@ export class LegacyErrorBridge { if (event.type === 'diagnosticEmitted') { const diagnosticId: string | undefined = (event.payload as { diagnosticId?: string }).diagnosticId; if (diagnosticId !== undefined) { - this._emittedDiagnosticIds.add(diagnosticId); + this.#emittedDiagnosticIds.add(diagnosticId); } } } @@ -121,11 +121,11 @@ export class LegacyErrorBridge { return true; } if (error instanceof RushError) { - return this._emittedDiagnosticIds.has(error.diagnosticId); + return this.#emittedDiagnosticIds.has(error.diagnosticId); } const correlated: string | undefined = this.getCorrelatedDiagnosticId(error); if (correlated !== undefined) { - return this._emittedDiagnosticIds.has(correlated); + return this.#emittedDiagnosticIds.has(correlated); } return false; } diff --git a/libraries/reporter/src/compat/LegacyFallbackSink.ts b/libraries/reporter/src/compat/LegacyFallbackSink.ts index 4c6aa5b4ed6..06647cd2288 100644 --- a/libraries/reporter/src/compat/LegacyFallbackSink.ts +++ b/libraries/reporter/src/compat/LegacyFallbackSink.ts @@ -15,10 +15,10 @@ import type { IReporterEventSink } from '../producers/IReporterEventSink'; * @beta */ export class LegacyFallbackSink implements IReporterEventSink { - private _nextId: number = 1; + #nextId: number = 1; public emit(): string { - return `discarded_${this._nextId++}`; + return `discarded_${this.#nextId++}`; } } diff --git a/libraries/reporter/src/compat/OldEngineOutputAdapter.ts b/libraries/reporter/src/compat/OldEngineOutputAdapter.ts index 06d6e3ecee1..59de493db08 100644 --- a/libraries/reporter/src/compat/OldEngineOutputAdapter.ts +++ b/libraries/reporter/src/compat/OldEngineOutputAdapter.ts @@ -54,18 +54,18 @@ export interface IOldEngineOutputAdapterOptions { * @beta */ export class OldEngineOutputAdapter { - private readonly _sink: IReporterEventSink; - private readonly _sessionId: string; - private readonly _source: IReporterEventSource; - private readonly _protocolVersion: IReporterProtocolVersion; - private readonly _maxChunkBytes: number; + readonly #sink: IReporterEventSink; + readonly #sessionId: string; + readonly #source: IReporterEventSource; + readonly #protocolVersion: IReporterProtocolVersion; + readonly #maxChunkBytes: number; public constructor(options: IOldEngineOutputAdapterOptions) { - this._sink = options.sink; - this._sessionId = options.sessionId; - this._source = options.source; - this._protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION; - this._maxChunkBytes = options.maxChunkBytes ?? REPORTER_PROTOCOL_LIMITS.externalOutputChunkBytes; + this.#sink = options.sink; + this.#sessionId = options.sessionId; + this.#source = options.source; + this.#protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION; + this.#maxChunkBytes = options.maxChunkBytes ?? REPORTER_PROTOCOL_LIMITS.externalOutputChunkBytes; } /** @@ -76,12 +76,12 @@ export class OldEngineOutputAdapter { */ public capture(stream: 'stdout' | 'stderr', text: string): string[] { const eventIds: string[] = []; - for (const chunk of chunkUtf8Text(text, this._maxChunkBytes)) { + for (const chunk of chunkUtf8Text(text, this.#maxChunkBytes)) { eventIds.push( - this._sink.emit({ - protocolVersion: this._protocolVersion, - sessionId: this._sessionId, - source: this._source, + this.#sink.emit({ + protocolVersion: this.#protocolVersion, + sessionId: this.#sessionId, + source: this.#source, privacy: 'local-sensitive', type: 'externalOutput', payload: { stream, text: chunk } diff --git a/libraries/reporter/src/frontend/ReporterHost.ts b/libraries/reporter/src/frontend/ReporterHost.ts index 0b6acdd3787..83b27ae25bd 100644 --- a/libraries/reporter/src/frontend/ReporterHost.ts +++ b/libraries/reporter/src/frontend/ReporterHost.ts @@ -11,10 +11,7 @@ import type { ReporterEventType } from '../events/ReporterEventType'; import type { IReporterEventSink } from '../producers/IReporterEventSink'; import { REPORTER_EVENT_TYPES } from '../events/ReporterEventType'; import { ReporterManager } from '../manager/ReporterManager'; -import { - REPORTER_PROTOCOL_VERSION, - isReporterProtocolCompatible -} from '../protocol/ReporterProtocol'; +import { REPORTER_PROTOCOL_VERSION, isReporterProtocolCompatible } from '../protocol/ReporterProtocol'; import { RUSH_REPORTER_BOOTSTRAP_HANDOFF_ENV_VAR, RUSH_REPORTER_BOOTSTRAP_NONCE_ENV_VAR @@ -101,7 +98,12 @@ export interface IBootstrapReplayResult { * The reason no events were replayed, when a handoff path was present. * `nonce-mismatch` means the file failed authentication and was rejected. */ - readonly skipReason?: 'unreadable' | 'invalid-path' | 'nonce-mismatch' | 'invalid-event' | 'incompatible-protocol'; + readonly skipReason?: + | 'unreadable' + | 'invalid-path' + | 'nonce-mismatch' + | 'invalid-event' + | 'incompatible-protocol'; } function isRecord(value: unknown): value is Record { @@ -156,25 +158,25 @@ function isReporterEventEnvelope(value: unknown): value is IReporterEventEnvelop * @beta */ export class ReporterHost { - private readonly _manager: ReporterManager; - private readonly _env: Record; - private readonly _handoffDirectory: string; - private readonly _retentionMs: number; - private readonly _nowMs: () => number; + readonly #manager: ReporterManager; + readonly #env: Record; + readonly #handoffDirectory: string; + readonly #retentionMs: number; + readonly #nowMs: () => number; public constructor(options: IReporterHostOptions = {}) { - this._manager = options.manager ?? new ReporterManager(); - this._env = options.env ?? process.env; - this._handoffDirectory = options.handoffDirectory ?? os.tmpdir(); - this._retentionMs = options.retentionMs ?? DEFAULT_HANDOFF_RETENTION_MS; - this._nowMs = options.nowMs ?? (() => Date.now()); + this.#manager = options.manager ?? new ReporterManager(); + this.#env = options.env ?? process.env; + this.#handoffDirectory = options.handoffDirectory ?? os.tmpdir(); + this.#retentionMs = options.retentionMs ?? DEFAULT_HANDOFF_RETENTION_MS; + this.#nowMs = options.nowMs ?? (() => Date.now()); } /** * The manager the host owns, used by the frontend to register reporters. */ public get manager(): ReporterManager { - return this._manager; + return this.#manager; } /** @@ -185,7 +187,7 @@ export class ReporterHost { * cannot register reporters, flush, or otherwise own selection. */ public getSink(): IReporterEventSink { - return this._manager; + return this.#manager; } /** @@ -200,12 +202,12 @@ export class ReporterHost { * handoff in the configured directory, and the header nonce must match. */ public async replayBootstrapHandoffAsync(): Promise { - const handoffPath: string | undefined = this._env[RUSH_REPORTER_BOOTSTRAP_HANDOFF_ENV_VAR]; + const handoffPath: string | undefined = this.#env[RUSH_REPORTER_BOOTSTRAP_HANDOFF_ENV_VAR]; if (!handoffPath) { return { direct: true, replayed: false, eventCount: 0 }; } - if (!this._isOwnedHandoffPath(handoffPath)) { + if (!this.#isOwnedHandoffPath(handoffPath)) { return { direct: false, replayed: false, @@ -215,7 +217,7 @@ export class ReporterHost { }; } - const expectedNonce: string | undefined = this._env[RUSH_REPORTER_BOOTSTRAP_NONCE_ENV_VAR]; + const expectedNonce: string | undefined = this.#env[RUSH_REPORTER_BOOTSTRAP_NONCE_ENV_VAR]; if (!expectedNonce) { return { direct: false, @@ -247,10 +249,7 @@ export class ReporterHost { let skippedEventCount: number = discardedRecordCount; for (const event of events) { const protocolVersion: IReporterProtocolVersion | undefined = getProtocolVersion(event); - if ( - protocolVersion && - !isReporterProtocolCompatible(REPORTER_PROTOCOL_VERSION, protocolVersion) - ) { + if (protocolVersion && !isReporterProtocolCompatible(REPORTER_PROTOCOL_VERSION, protocolVersion)) { await deleteBootstrapHandoffFileAsync(handoffPath); return { direct: false, @@ -291,7 +290,7 @@ export class ReporterHost { try { for (const event of acceptedEvents) { - this._manager.ingestForeignEnvelope(event); + this.#manager.ingestForeignEnvelope(event); } } finally { await deleteBootstrapHandoffFileAsync(handoffPath); @@ -314,17 +313,17 @@ export class ReporterHost { const deleted: string[] = []; let fileNames: string[]; try { - fileNames = await fs.promises.readdir(this._handoffDirectory); + fileNames = await fs.promises.readdir(this.#handoffDirectory); } catch { return deleted; } - const cutoff: number = this._nowMs() - this._retentionMs; + const cutoff: number = this.#nowMs() - this.#retentionMs; for (const fileName of fileNames) { if (!isBootstrapHandoffFileName(fileName)) { continue; } - const filePath: string = path.join(this._handoffDirectory, fileName); + const filePath: string = path.join(this.#handoffDirectory, fileName); try { const stats: fs.Stats = await fs.promises.stat(filePath); if (stats.mtimeMs < cutoff) { @@ -338,10 +337,10 @@ export class ReporterHost { return deleted; } - private _isOwnedHandoffPath(handoffPath: string): boolean { + #isOwnedHandoffPath(handoffPath: string): boolean { const resolvedPath: string = path.resolve(handoffPath); return ( - path.dirname(resolvedPath) === path.resolve(this._handoffDirectory) && + path.dirname(resolvedPath) === path.resolve(this.#handoffDirectory) && isBootstrapHandoffFileName(path.basename(resolvedPath)) ); } diff --git a/libraries/reporter/src/heft/HeftChildEmitter.ts b/libraries/reporter/src/heft/HeftChildEmitter.ts index 0178e5cefa1..e669a68ae5f 100644 --- a/libraries/reporter/src/heft/HeftChildEmitter.ts +++ b/libraries/reporter/src/heft/HeftChildEmitter.ts @@ -109,53 +109,53 @@ export class HeftChildEmitter { */ public readonly mode: HeftChildReporterMode; - private readonly _writeDescriptor: ((text: string) => void) | undefined; - private readonly _writeStdout: ((text: string) => void) | undefined; - private readonly _writeStderr: ((text: string) => void) | undefined; - private readonly _childSessionId: string; - private readonly _source: IReporterEventSource; - private readonly _producerVersion: string; - private readonly _protocolVersion: IReporterProtocolVersion; - private readonly _capabilities: readonly string[]; - private readonly _requiredFeatures: readonly string[]; - private readonly _now: () => string; - private _sequence: number; - private _nextEventId: number; + readonly #writeDescriptor: ((text: string) => void) | undefined; + readonly #writeStdout: ((text: string) => void) | undefined; + readonly #writeStderr: ((text: string) => void) | undefined; + readonly #childSessionId: string; + readonly #source: IReporterEventSource; + readonly #producerVersion: string; + readonly #protocolVersion: IReporterProtocolVersion; + readonly #capabilities: readonly string[]; + readonly #requiredFeatures: readonly string[]; + readonly #now: () => string; + #sequence: number; + #nextEventId: number; public constructor(options: IHeftChildEmitterOptions) { const fd: number | undefined = readChildDescriptorFd(options.env); delete options.env[RUSH_REPORTER_CHILD_FD_ENV_VAR]; this.mode = fd !== undefined && options.writeDescriptor !== undefined ? 'structured' : 'raw-fallback'; - this._writeDescriptor = options.writeDescriptor; - this._writeStdout = options.writeStdout; - this._writeStderr = options.writeStderr; - this._childSessionId = options.childSessionId; - this._source = options.source; - this._producerVersion = options.producerVersion; - this._protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION; - this._capabilities = options.capabilities ?? []; - this._requiredFeatures = options.requiredFeatures ?? []; - this._now = options.now ?? (() => new Date().toISOString()); - this._sequence = 1; - this._nextEventId = 1; + this.#writeDescriptor = options.writeDescriptor; + this.#writeStdout = options.writeStdout; + this.#writeStderr = options.writeStderr; + this.#childSessionId = options.childSessionId; + this.#source = options.source; + this.#producerVersion = options.producerVersion; + this.#protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION; + this.#capabilities = options.capabilities ?? []; + this.#requiredFeatures = options.requiredFeatures ?? []; + this.#now = options.now ?? (() => new Date().toISOString()); + this.#sequence = 1; + this.#nextEventId = 1; } /** * Sends the hello handshake over the descriptor. Returns `false` in fallback mode. */ public sendHello(): boolean { - if (this.mode !== 'structured' || this._writeDescriptor === undefined) { + if (this.mode !== 'structured' || this.#writeDescriptor === undefined) { return false; } const hello: IReporterHello = { kind: 'hello', - protocolVersion: this._protocolVersion, - producerVersion: this._producerVersion, - capabilities: [...this._capabilities], - requiredFeatures: [...this._requiredFeatures] + protocolVersion: this.#protocolVersion, + producerVersion: this.#producerVersion, + capabilities: [...this.#capabilities], + requiredFeatures: [...this.#requiredFeatures] }; - this._writeDescriptor(encodeNdjsonRecord(hello)); + this.#writeDescriptor(encodeNdjsonRecord(hello)); return true; } @@ -164,24 +164,24 @@ export class HeftChildEmitter { * `undefined` in fallback mode. */ public emitEvent(input: IHeftChildEventInput): string | undefined { - if (this.mode !== 'structured' || this._writeDescriptor === undefined) { + if (this.mode !== 'structured' || this.#writeDescriptor === undefined) { return undefined; } - const eventId: string = `child_${this._nextEventId++}`; + const eventId: string = `child_${this.#nextEventId++}`; const envelope: Record = { - protocolVersion: this._protocolVersion, + protocolVersion: this.#protocolVersion, eventId, - sessionId: this._childSessionId, - sequence: this._sequence++, - timestamp: this._now(), - source: this._source, + sessionId: this.#childSessionId, + sequence: this.#sequence++, + timestamp: this.#now(), + source: this.#source, scope: input.scope, privacy: input.privacy ?? 'public', required: isReporterEventRequired(input.type), type: input.type, payload: input.payload ?? {} }; - this._writeDescriptor(encodeNdjsonRecord(envelope)); + this.#writeDescriptor(encodeNdjsonRecord(envelope)); return eventId; } @@ -190,9 +190,9 @@ export class HeftChildEmitter { */ public writeRaw(stream: 'stdout' | 'stderr', text: string): void { if (stream === 'stderr') { - this._writeStderr?.(text); + this.#writeStderr?.(text); } else { - this._writeStdout?.(text); + this.#writeStdout?.(text); } } } diff --git a/libraries/reporter/src/heft/HeftDescriptorHost.ts b/libraries/reporter/src/heft/HeftDescriptorHost.ts index 52ad8feb4a3..e77ea2cd49f 100644 --- a/libraries/reporter/src/heft/HeftDescriptorHost.ts +++ b/libraries/reporter/src/heft/HeftDescriptorHost.ts @@ -183,24 +183,24 @@ export interface IHeftChildResult { * @beta */ export class HeftDescriptorHost { - private readonly _parentSessionId: string; - private readonly _parentOperationId: string | undefined; - private readonly _supportedProtocolVersion: IReporterProtocolVersion; - private readonly _supportedCapabilities: readonly string[] | undefined; - private readonly _forwardEnvelope: (envelope: IReporterEventEnvelope) => void; - private readonly _onNegotiation: ((result: IReporterHandshakeResult) => void) | undefined; + readonly #parentSessionId: string; + readonly #parentOperationId: string | undefined; + readonly #supportedProtocolVersion: IReporterProtocolVersion; + readonly #supportedCapabilities: readonly string[] | undefined; + readonly #forwardEnvelope: (envelope: IReporterEventEnvelope) => void; + readonly #onNegotiation: ((result: IReporterHandshakeResult) => void) | undefined; - private _negotiation: IReporterHandshakeResult | undefined; - private _protocolFailure: IRushDiagnostic | undefined; - private _eventCount: number = 0; + #negotiation: IReporterHandshakeResult | undefined; + #protocolFailure: IRushDiagnostic | undefined; + #eventCount: number = 0; public constructor(options: IHeftDescriptorHostOptions) { - this._parentSessionId = options.parentSessionId; - this._parentOperationId = options.parentOperationId; - this._supportedProtocolVersion = options.supportedProtocolVersion; - this._supportedCapabilities = options.supportedCapabilities; - this._forwardEnvelope = options.forwardEnvelope; - this._onNegotiation = options.onNegotiation; + this.#parentSessionId = options.parentSessionId; + this.#parentOperationId = options.parentOperationId; + this.#supportedProtocolVersion = options.supportedProtocolVersion; + this.#supportedCapabilities = options.supportedCapabilities; + this.#forwardEnvelope = options.forwardEnvelope; + this.#onNegotiation = options.onNegotiation; } /** @@ -213,50 +213,50 @@ export class HeftDescriptorHost { * is accepted. */ public processChildRecord(record: unknown): boolean { - if (this._protocolFailure !== undefined) { + if (this.#protocolFailure !== undefined) { return false; } - if (this._negotiation === undefined) { + if (this.#negotiation === undefined) { if (!isReporterHello(record)) { - return this._rejectMalformedStream('the first record was not a valid hello'); + return this.#rejectMalformedStream('the first record was not a valid hello'); } const result: IReporterHandshakeResult = negotiateReporterHello(record, { - supportedProtocolVersion: this._supportedProtocolVersion, - supportedCapabilities: this._supportedCapabilities + supportedProtocolVersion: this.#supportedProtocolVersion, + supportedCapabilities: this.#supportedCapabilities }); - this._negotiation = result; - this._onNegotiation?.(result); + this.#negotiation = result; + this.#onNegotiation?.(result); return result.accepted; } - if (!this._negotiation.accepted) { + if (!this.#negotiation.accepted) { return false; } if (!isReporterEventRecord(record)) { - return this._rejectMalformedStream('an event record did not contain a valid reporter envelope'); + return this.#rejectMalformedStream('an event record did not contain a valid reporter envelope'); } - if (record.protocolVersion.major !== this._negotiation.ack.protocolVersion.major) { - return this._rejectMalformedStream( + if (record.protocolVersion.major !== this.#negotiation.ack.protocolVersion.major) { + return this.#rejectMalformedStream( 'an event record used a protocol major different from the negotiated stream' ); } if (!isReporterEventType(record.type)) { if (record.required) { - return this._rejectMalformedStream('a required event type was not recognized'); + return this.#rejectMalformedStream('a required event type was not recognized'); } return true; } const correlated: IReporterEventEnvelope = { ...record, - parentSessionId: this._parentSessionId, - parentOperationId: this._parentOperationId, + parentSessionId: this.#parentSessionId, + parentOperationId: this.#parentOperationId, required: isReporterEventRequired(record.type), type: record.type }; - this._forwardEnvelope(correlated); - this._eventCount++; + this.#forwardEnvelope(correlated); + this.#eventCount++; return true; } @@ -273,14 +273,14 @@ export class HeftDescriptorHost { const decoder: NdjsonDecoder = new NdjsonDecoder(); return { write: (chunk: string): void => { - if (this._protocolFailure !== undefined || this._negotiation?.accepted === false) { + if (this.#protocolFailure !== undefined || this.#negotiation?.accepted === false) { return; } let records: unknown[]; try { records = decoder.decode(chunk); } catch { - this._rejectMalformedStream('its NDJSON could not be decoded within the protocol limits'); + this.#rejectMalformedStream('its NDJSON could not be decoded within the protocol limits'); return; } for (const record of records) { @@ -288,20 +288,20 @@ export class HeftDescriptorHost { } }, flush: (): IHeftChildResult => { - if (this._protocolFailure === undefined && this._negotiation?.accepted !== false) { + if (this.#protocolFailure === undefined && this.#negotiation?.accepted !== false) { let records: unknown[]; try { records = decoder.flush(); } catch { - this._rejectMalformedStream('its trailing NDJSON record was invalid'); - return this._result(); + this.#rejectMalformedStream('its trailing NDJSON record was invalid'); + return this.#result(); } for (const record of records) { this.processChildRecord(record); } } - return this._result(); + return this.#result(); } }; } @@ -313,7 +313,7 @@ export class HeftDescriptorHost { for (const record of records) { this.processChildRecord(record); } - return this._result(); + return this.#result(); } /** @@ -329,45 +329,45 @@ export class HeftDescriptorHost { return processor.flush(); } - private _result(): IHeftChildResult { - const negotiation: IReporterHandshakeResult | undefined = this._negotiation; + #result(): IHeftChildResult { + const negotiation: IReporterHandshakeResult | undefined = this.#negotiation; if (negotiation === undefined) { return { accepted: false, eventCount: 0 }; } return { - accepted: negotiation.accepted && this._protocolFailure === undefined, - eventCount: this._eventCount, + accepted: negotiation.accepted && this.#protocolFailure === undefined, + eventCount: this.#eventCount, ...('ack' in negotiation && negotiation.ack !== undefined ? { ack: negotiation.ack } : {}), - ...(this._protocolFailure !== undefined - ? { diagnostic: this._protocolFailure } + ...(this.#protocolFailure !== undefined + ? { diagnostic: this.#protocolFailure } : 'diagnostic' in negotiation && negotiation.diagnostic !== undefined ? { diagnostic: negotiation.diagnostic } : {}) }; } - private _rejectMalformedStream(reason: string): false { - if (this._protocolFailure === undefined) { - this._protocolFailure = createRushDiagnostic('RUSH_PROTOCOL_INVALID_CHILD_STREAM', { + #rejectMalformedStream(reason: string): false { + if (this.#protocolFailure === undefined) { + this.#protocolFailure = createRushDiagnostic('RUSH_PROTOCOL_INVALID_CHILD_STREAM', { parameters: { reason: { value: reason, privacy: 'public' } } }); } - if (this._negotiation === undefined) { + if (this.#negotiation === undefined) { const result: IReporterHandshakeResult = { accepted: false, ack: { kind: 'helloAck', - protocolVersion: this._supportedProtocolVersion, + protocolVersion: this.#supportedProtocolVersion, acceptedCapabilities: [], rejectedRequiredFeatures: [] }, - diagnostic: this._protocolFailure + diagnostic: this.#protocolFailure }; - this._negotiation = result; - this._onNegotiation?.(result); + this.#negotiation = result; + this.#onNegotiation?.(result); } return false; } diff --git a/libraries/reporter/src/lifecycle/LifecycleEmitter.ts b/libraries/reporter/src/lifecycle/LifecycleEmitter.ts index 00497c69ec1..e343721e8f8 100644 --- a/libraries/reporter/src/lifecycle/LifecycleEmitter.ts +++ b/libraries/reporter/src/lifecycle/LifecycleEmitter.ts @@ -64,38 +64,38 @@ export interface ILifecycleEmitterOptions { * @beta */ export class LifecycleEmitter { - private readonly _sink: IReporterEventSink; - private readonly _sessionId: string; - private readonly _source: IReporterEventSource; - private readonly _scope: IReporterEventScope | undefined; - private readonly _protocolVersion: IReporterProtocolVersion; + readonly #sink: IReporterEventSink; + readonly #sessionId: string; + readonly #source: IReporterEventSource; + readonly #scope: IReporterEventScope | undefined; + readonly #protocolVersion: IReporterProtocolVersion; public constructor(options: ILifecycleEmitterOptions) { - this._sink = options.sink; - this._sessionId = options.sessionId; - this._source = options.source; - this._scope = options.scope; - this._protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION; + this.#sink = options.sink; + this.#sessionId = options.sessionId; + this.#source = options.source; + this.#scope = options.scope; + this.#protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION; } public emitSessionStarted(payload: ISessionStartedPayload): string { - return this._emit('sessionStarted', payload, 'public'); + return this.#emit('sessionStarted', payload, 'public'); } public emitSessionCompleted(payload: ISessionCompletedPayload): string { - return this._emit('sessionCompleted', payload, 'public'); + return this.#emit('sessionCompleted', payload, 'public'); } public emitCommandStarted(payload: ICommandStartedPayload): string { - return this._emit('commandStarted', payload, 'public', { commandName: payload.commandName }); + return this.#emit('commandStarted', payload, 'public', { commandName: payload.commandName }); } public emitCommandCompleted(payload: ICommandCompletedPayload): string { - return this._emit('commandCompleted', payload, 'public', { commandName: payload.commandName }); + return this.#emit('commandCompleted', payload, 'public', { commandName: payload.commandName }); } public emitOperationRegistered(payload: IOperationRegisteredPayload): string { - return this._emit('operationRegistered', payload, 'public', { + return this.#emit('operationRegistered', payload, 'public', { operationId: payload.operationId, ...(payload.projectName === undefined ? {} : { projectName: payload.projectName }), ...(payload.phaseName === undefined ? {} : { phaseName: payload.phaseName }) @@ -103,17 +103,17 @@ export class LifecycleEmitter { } public emitOperationStatusChanged(payload: IOperationStatusChangedPayload): string { - return this._emit('operationStatusChanged', payload, 'public', { + return this.#emit('operationStatusChanged', payload, 'public', { operationId: payload.operationId }); } public emitCommandResult(payload: ICommandResultPayload): string { - return this._emit('commandResult', payload, 'public', { commandName: payload.commandName }); + return this.#emit('commandResult', payload, 'public', { commandName: payload.commandName }); } public emitWatchCycleCompleted(payload: IWatchCycleCompletedPayload): string { - return this._emit('watchCycleCompleted', payload, 'public'); + return this.#emit('watchCycleCompleted', payload, 'public'); } /** @@ -123,10 +123,10 @@ export class LifecycleEmitter { const classifications: ReadonlyArray<'public' | 'local-sensitive' | 'secret'> = diagnostic.parameters ? Object.values(diagnostic.parameters).map((value) => value.privacy) : []; - return this._emit('diagnosticEmitted', diagnostic, computeEnvelopePrivacyFloor(classifications)); + return this.#emit('diagnosticEmitted', diagnostic, computeEnvelopePrivacyFloor(classifications)); } - private _emit( + #emit( type: | 'sessionStarted' | 'sessionCompleted' @@ -142,11 +142,11 @@ export class LifecycleEmitter { scopeOverride?: IReporterEventScope ): string { const scope: IReporterEventScope | undefined = - this._scope || scopeOverride ? { ...this._scope, ...scopeOverride } : undefined; - return this._sink.emit({ - protocolVersion: this._protocolVersion, - sessionId: this._sessionId, - source: this._source, + this.#scope || scopeOverride ? { ...this.#scope, ...scopeOverride } : undefined; + return this.#sink.emit({ + protocolVersion: this.#protocolVersion, + sessionId: this.#sessionId, + source: this.#source, scope, privacy, type, diff --git a/libraries/reporter/src/manager/ReporterManager.ts b/libraries/reporter/src/manager/ReporterManager.ts index 657e88c17a6..ec9ab47addf 100644 --- a/libraries/reporter/src/manager/ReporterManager.ts +++ b/libraries/reporter/src/manager/ReporterManager.ts @@ -111,16 +111,16 @@ interface IReporterEntry { * @beta */ export class ReporterManager implements IReporterEventSink { - private readonly _entries: IReporterEntry[]; - private readonly _ownedDestinations: Set; - private readonly _protocolVersion: IReporterProtocolVersion; - private readonly _now: () => string; - private readonly _coalesceThreshold: number; - private readonly _emergencyDiagnosticWriter: (message: string) => void; - private _nextSequence: number; - private _nextEventId: number; - private _initialized: boolean; - private _fatalError: Error | undefined; + readonly #entries: IReporterEntry[]; + readonly #ownedDestinations: Set; + readonly #protocolVersion: IReporterProtocolVersion; + readonly #now: () => string; + readonly #coalesceThreshold: number; + readonly #emergencyDiagnosticWriter: (message: string) => void; + #nextSequence: number; + #nextEventId: number; + #initialized: boolean; + #fatalError: Error | undefined; public constructor(options: IReporterManagerOptions = {}) { const { @@ -131,19 +131,19 @@ export class ReporterManager implements IReporterEventSink { process.stderr.write(`${message}\n`); } } = options; - this._entries = []; - this._ownedDestinations = new Set(); - this._protocolVersion = protocolVersion; - this._now = now; + this.#entries = []; + this.#ownedDestinations = new Set(); + this.#protocolVersion = protocolVersion; + this.#now = now; if (!Number.isSafeInteger(coalesceThreshold) || coalesceThreshold < 1) { throw new RangeError('coalesceThreshold must be a positive integer.'); } - this._coalesceThreshold = coalesceThreshold; - this._emergencyDiagnosticWriter = emergencyDiagnosticWriter; - this._nextSequence = 1; - this._nextEventId = 1; - this._initialized = false; - this._fatalError = undefined; + this.#coalesceThreshold = coalesceThreshold; + this.#emergencyDiagnosticWriter = emergencyDiagnosticWriter; + this.#nextSequence = 1; + this.#nextEventId = 1; + this.#initialized = false; + this.#fatalError = undefined; } /** @@ -152,20 +152,20 @@ export class ReporterManager implements IReporterEventSink { * @throws Error if the destination is already owned, or if called after initialization */ public addReporter(reporter: IReporter, options: IReporterRegistrationOptions = {}): void { - if (this._initialized) { + if (this.#initialized) { throw new Error('Reporters cannot be added after the manager is initialized.'); } const destination: string | undefined = options.destination; if (destination !== undefined) { - if (this._ownedDestinations.has(destination)) { + if (this.#ownedDestinations.has(destination)) { throw new Error( `The destination ${JSON.stringify(destination)} is already owned by another reporter. ` + `Share a destination only through an explicit multiplexer.` ); } - this._ownedDestinations.add(destination); + this.#ownedDestinations.add(destination); } - this._entries.push({ + this.#entries.push({ reporter, destination, required: options.required ?? false, @@ -186,14 +186,14 @@ export class ReporterManager implements IReporterEventSink { * reporter's error. */ public async initializeAsync(): Promise { - for (const entry of this._entries) { + for (const entry of this.#entries) { const context: IReporterContext = { - protocolVersion: this._protocolVersion, + protocolVersion: this.#protocolVersion, destination: entry.destination }; await entry.reporter.initializeAsync(context); } - this._initialized = true; + this.#initialized = true; } /** @@ -205,16 +205,16 @@ export class ReporterManager implements IReporterEventSink { * {@link isReporterEventRequired}; producers never set it. */ public emit(event: IReporterEmitEventInput): string { - this._ensureInitialized(); - const eventId: string = `evt_${this._nextEventId++}`; + this.#ensureInitialized(); + const eventId: string = `evt_${this.#nextEventId++}`; const envelope: IReporterEventEnvelope = { ...event, required: isReporterEventRequired(event.type), eventId, - sequence: this._nextSequence++, - timestamp: this._now() + sequence: this.#nextSequence++, + timestamp: this.#now() }; - this._fanOut(envelope); + this.#fanOut(envelope); return eventId; } @@ -228,14 +228,14 @@ export class ReporterManager implements IReporterEventSink { * @returns the ingested event's `eventId` */ public ingestForeignEnvelope(envelope: IReporterEventEnvelope): string { - this._ensureInitialized(); + this.#ensureInitialized(); const rehomed: IReporterEventEnvelope = { ...envelope, required: isReporterEventRequired(envelope.type), - sequence: this._nextSequence++, + sequence: this.#nextSequence++, sourceSequence: envelope.sequence }; - this._fanOut(rehomed); + this.#fanOut(rehomed); return rehomed.eventId; } @@ -251,7 +251,7 @@ export class ReporterManager implements IReporterEventSink { */ public getPendingEventCount(): number { let total: number = 0; - for (const entry of this._entries) { + for (const entry of this.#entries) { total += entry.queue.length; } return total; @@ -264,14 +264,14 @@ export class ReporterManager implements IReporterEventSink { * @throws the captured fatal error if a required reporter failed */ public async flushAsync(timeoutMs: number = DEFAULT_FLUSH_TIMEOUT_MS): Promise { - await this._settleAsync(async (entry: IReporterEntry): Promise => { + await this.#settleAsync(async (entry: IReporterEntry): Promise => { await entry.drainPromise; if (!entry.disabled) { await entry.reporter.flushAsync(); } }, timeoutMs); - if (this._fatalError) { - throw this._fatalError; + if (this.#fatalError) { + throw this.#fatalError; } } @@ -283,7 +283,7 @@ export class ReporterManager implements IReporterEventSink { * without risk. */ public async signalFlushAsync(timeoutMs: number = DEFAULT_SIGNAL_FLUSH_TIMEOUT_MS): Promise { - await this._settleAsync(async (entry: IReporterEntry): Promise => { + await this.#settleAsync(async (entry: IReporterEntry): Promise => { await entry.drainPromise; if (!entry.disabled) { await entry.reporter.flushAsync(); @@ -303,47 +303,47 @@ export class ReporterManager implements IReporterEventSink { } catch (error) { flushError = error as Error; } - await this._settleAsync(async (entry: IReporterEntry): Promise => { + await this.#settleAsync(async (entry: IReporterEntry): Promise => { await entry.reporter.closeAsync(); }, timeoutMs); if (flushError) { throw flushError; } - if (this._fatalError) { - throw this._fatalError; + if (this.#fatalError) { + throw this.#fatalError; } } - private _fanOut(envelope: IReporterEventEnvelope): void { - for (const entry of this._entries) { + #fanOut(envelope: IReporterEventEnvelope): void { + for (const entry of this.#entries) { if (!entry.disabled) { - this._enqueue(entry, envelope); + this.#enqueue(entry, envelope); } } } - private _ensureInitialized(): void { - if (!this._initialized) { + #ensureInitialized(): void { + if (!this.#initialized) { throw new Error('ReporterManager must be initialized before publishing events.'); } } - private _enqueue(entry: IReporterEntry, envelope: IReporterEventEnvelope): void { + #enqueue(entry: IReporterEntry, envelope: IReporterEventEnvelope): void { const lastIndex: number = entry.queue.length - 1; if ( - entry.queue.length >= this._coalesceThreshold && - this._isCoalescibleStatusEvent(envelope) && + entry.queue.length >= this.#coalesceThreshold && + this.#isCoalescibleStatusEvent(envelope) && lastIndex >= 0 && - this._isCoalescibleStatusEvent(entry.queue[lastIndex]) + this.#isCoalescibleStatusEvent(entry.queue[lastIndex]) ) { // Under pressure, a replaceable status event supersedes the previous // unsent one instead of growing the queue. Protected events are never // coalesced or dropped. entry.queue[lastIndex] = envelope; } else { - if (entry.queue.length >= this._coalesceThreshold) { + if (entry.queue.length >= this.#coalesceThreshold) { const oldestEnvelope: IReporterEventEnvelope = entry.queue.shift()!; - this._deliverEnvelope(entry, oldestEnvelope); + this.#deliverEnvelope(entry, oldestEnvelope); if (entry.disabled) { entry.queue.length = 0; return; @@ -354,15 +354,15 @@ export class ReporterManager implements IReporterEventSink { if (!entry.draining) { entry.draining = true; - entry.drainPromise = this._drainEntryAsync(entry); + entry.drainPromise = this.#drainEntryAsync(entry); } } - private async _drainEntryAsync(entry: IReporterEntry): Promise { + async #drainEntryAsync(entry: IReporterEntry): Promise { try { while (entry.queue.length > 0) { const envelope: IReporterEventEnvelope = entry.queue.shift()!; - this._deliverEnvelope(entry, envelope); + this.#deliverEnvelope(entry, envelope); if (entry.disabled) { entry.queue.length = 0; break; @@ -375,47 +375,46 @@ export class ReporterManager implements IReporterEventSink { } } - private _deliverEnvelope(entry: IReporterEntry, envelope: IReporterEventEnvelope): void { + #deliverEnvelope(entry: IReporterEntry, envelope: IReporterEventEnvelope): void { try { entry.reporter.report(envelope); } catch (error) { - this._handleReporterFailure(entry, error as Error); + this.#handleReporterFailure(entry, error as Error); } } - private _handleReporterFailure(entry: IReporterEntry, error: Error): void { + #handleReporterFailure(entry: IReporterEntry, error: Error): void { if (entry.required) { - if (!this._fatalError) { - this._fatalError = error; + if (!this.#fatalError) { + this.#fatalError = error; } // Write the emergency diagnostic once; a failed required reporter keeps // receiving events until teardown, and a per-event line would spam stderr. if (!entry.failureNotified) { entry.failureNotified = true; - this._emergencyDiagnosticWriter( + this.#emergencyDiagnosticWriter( `[reporter] Required reporter ${JSON.stringify(entry.reporter.name)} failed: ${error.message}` ); } return; } entry.disabled = true; - this._emergencyDiagnosticWriter( - `[reporter] Disabling optional reporter ${JSON.stringify(entry.reporter.name)} after failure: ${error.message}` + this.#emergencyDiagnosticWriter( + `[reporter] Disabling optional reporter ${JSON.stringify(entry.reporter.name)} after failure: ${ + error.message + }` ); } - private _isCoalescibleStatusEvent(envelope: IReporterEventEnvelope): boolean { + #isCoalescibleStatusEvent(envelope: IReporterEventEnvelope): boolean { // Only non-required activity/liveness events are replaceable. Every other // event type, and any required event, must be delivered. return envelope.type === 'activityChanged' && !envelope.required; } - private async _settleAsync( - action: (entry: IReporterEntry) => Promise, - timeoutMs: number - ): Promise { + async #settleAsync(action: (entry: IReporterEntry) => Promise, timeoutMs: number): Promise { const work: Promise = Promise.all( - this._entries.map((entry: IReporterEntry) => this._scheduleLifecycleAction(entry, action)) + this.#entries.map((entry: IReporterEntry) => this.#scheduleLifecycleAction(entry, action)) ).then(() => undefined); let timer: ReturnType | undefined; @@ -432,13 +431,13 @@ export class ReporterManager implements IReporterEventSink { } } - private _scheduleLifecycleAction( + #scheduleLifecycleAction( entry: IReporterEntry, action: (entry: IReporterEntry) => Promise ): Promise { const scheduled: Promise = entry.lifecyclePromise.then(() => action(entry)); const settled: Promise = scheduled.catch((error: Error) => { - this._handleReporterFailure(entry, error); + this.#handleReporterFailure(entry, error); }); entry.lifecyclePromise = settled; return settled; diff --git a/libraries/reporter/src/manager/ReporterMultiplexer.ts b/libraries/reporter/src/manager/ReporterMultiplexer.ts index b10d53909d4..d4cc2d64ac2 100644 --- a/libraries/reporter/src/manager/ReporterMultiplexer.ts +++ b/libraries/reporter/src/manager/ReporterMultiplexer.ts @@ -22,33 +22,33 @@ export class ReporterMultiplexer implements IReporter { */ public readonly name: string; - private readonly _reporters: readonly IReporter[]; + readonly #reporters: readonly IReporter[]; public constructor(name: string, reporters: readonly IReporter[]) { this.name = name; - this._reporters = [...reporters]; + this.#reporters = [...reporters]; } public async initializeAsync(context: IReporterContext): Promise { - for (const reporter of this._reporters) { + for (const reporter of this.#reporters) { await reporter.initializeAsync(context); } } public report(event: IReporterEventEnvelope): void { - for (const reporter of this._reporters) { + for (const reporter of this.#reporters) { reporter.report(event); } } public async flushAsync(): Promise { - for (const reporter of this._reporters) { + for (const reporter of this.#reporters) { await reporter.flushAsync(); } } public async closeAsync(): Promise { - for (const reporter of this._reporters) { + for (const reporter of this.#reporters) { await reporter.closeAsync(); } } diff --git a/libraries/reporter/src/matchers/ProblemMatcherRegistry.ts b/libraries/reporter/src/matchers/ProblemMatcherRegistry.ts index 2cd272ecc83..16634483678 100644 --- a/libraries/reporter/src/matchers/ProblemMatcherRegistry.ts +++ b/libraries/reporter/src/matchers/ProblemMatcherRegistry.ts @@ -31,20 +31,20 @@ export interface IGetMatchersOptions { * @beta */ export class ProblemMatcherRegistry { - private readonly _matchers: IProblemMatcher[] = []; + readonly #matchers: IProblemMatcher[] = []; /** * Registers a matcher. */ public register(matcher: IProblemMatcher): void { - this._matchers.push(matcher); + this.#matchers.push(matcher); } /** * Returns the matchers that apply to a tool and version. */ public getMatchers(tool: string, options: IGetMatchersOptions = {}): IProblemMatcher[] { - return this._matchers.filter((matcher: IProblemMatcher) => { + return this.#matchers.filter((matcher: IProblemMatcher) => { if (matcher.tool !== tool) { return false; } diff --git a/libraries/reporter/src/protocol/Ndjson.ts b/libraries/reporter/src/protocol/Ndjson.ts index 909e981cf56..f2a4d50003b 100644 --- a/libraries/reporter/src/protocol/Ndjson.ts +++ b/libraries/reporter/src/protocol/Ndjson.ts @@ -106,12 +106,12 @@ export function encodeNdjsonRecord(value: unknown, options?: INdjsonOptions): st * @beta */ export class NdjsonDecoder { - private readonly _maxRecordBytes: number; - private _buffer: string; + readonly #maxRecordBytes: number; + #buffer: string; public constructor(options?: INdjsonOptions) { - this._maxRecordBytes = options?.maxRecordBytes ?? REPORTER_PROTOCOL_LIMITS.ndjsonRecordBytes; - this._buffer = ''; + this.#maxRecordBytes = options?.maxRecordBytes ?? REPORTER_PROTOCOL_LIMITS.ndjsonRecordBytes; + this.#buffer = ''; } /** @@ -122,20 +122,20 @@ export class NdjsonDecoder { * @throws {@link NdjsonInvalidRecordError} if a completed record is malformed */ public decode(chunk: string): unknown[] { - this._buffer += chunk; + this.#buffer += chunk; const records: unknown[] = []; - let newlineIndex: number = this._buffer.indexOf('\n'); + let newlineIndex: number = this.#buffer.indexOf('\n'); while (newlineIndex >= 0) { - const line: string = this._buffer.slice(0, newlineIndex); - this._buffer = this._buffer.slice(newlineIndex + 1); - this._processLine(line, records); - newlineIndex = this._buffer.indexOf('\n'); + const line: string = this.#buffer.slice(0, newlineIndex); + this.#buffer = this.#buffer.slice(newlineIndex + 1); + this.#processLine(line, records); + newlineIndex = this.#buffer.indexOf('\n'); } // A partial line that already exceeds the limit can never become a valid record. - if (Buffer.byteLength(this._buffer, 'utf8') > this._maxRecordBytes) { - throw new NdjsonRecordTooLargeError(this._maxRecordBytes, records); + if (Buffer.byteLength(this.#buffer, 'utf8') > this.#maxRecordBytes) { + throw new NdjsonRecordTooLargeError(this.#maxRecordBytes, records); } return records; @@ -149,17 +149,17 @@ export class NdjsonDecoder { */ public flush(): unknown[] { const records: unknown[] = []; - if (this._buffer.length > 0) { - const line: string = this._buffer; - this._buffer = ''; - this._processLine(line, records); + if (this.#buffer.length > 0) { + const line: string = this.#buffer; + this.#buffer = ''; + this.#processLine(line, records); } return records; } - private _processLine(line: string, records: unknown[]): void { - if (Buffer.byteLength(line, 'utf8') > this._maxRecordBytes) { - throw new NdjsonRecordTooLargeError(this._maxRecordBytes, records); + #processLine(line: string, records: unknown[]): void { + if (Buffer.byteLength(line, 'utf8') > this.#maxRecordBytes) { + throw new NdjsonRecordTooLargeError(this.#maxRecordBytes, records); } const trimmed: string = line.trim(); if (trimmed.length === 0) { diff --git a/libraries/reporter/src/reporters/AiReporter.ts b/libraries/reporter/src/reporters/AiReporter.ts index fc885273dc1..eaf7a54f23a 100644 --- a/libraries/reporter/src/reporters/AiReporter.ts +++ b/libraries/reporter/src/reporters/AiReporter.ts @@ -102,57 +102,57 @@ export interface IAiReporterOptions { export class AiReporter implements IReporter { public readonly name: string = 'ai'; - private readonly _write: (text: string) => void; - private readonly _maxBytes: number; - private readonly _maxDetailedDiagnostics: number; + readonly #write: (text: string) => void; + readonly #maxBytes: number; + readonly #maxDetailedDiagnostics: number; - private _protocolVersion: IReporterProtocolVersion; - private _commandName: string | undefined; - private readonly _projectByOperation: Map; - private readonly _operationCounts: { [status: string]: number }; - private readonly _failedProjects: string[]; - private readonly _errorDiagnostics: IAiDiagnostic[]; - private readonly _warningDiagnostics: IAiDiagnostic[]; - private readonly _errorCodes: Set; - private readonly _diagnosticCategoryCounts: { [category: string]: number }; - private _errorDiagnosticsTruncated: boolean; - private _warningDiagnosticsTruncated: boolean; - private _errorCount: number; - private _warningCount: number; - private _logPath: string | undefined; - private _logFormat: string | undefined; - private _artifactComplete: boolean; - private _finalEmitted: boolean; + #protocolVersion: IReporterProtocolVersion; + #commandName: string | undefined; + readonly #projectByOperation: Map; + readonly #operationCounts: { [status: string]: number }; + readonly #failedProjects: string[]; + readonly #errorDiagnostics: IAiDiagnostic[]; + readonly #warningDiagnostics: IAiDiagnostic[]; + readonly #errorCodes: Set; + readonly #diagnosticCategoryCounts: { [category: string]: number }; + #errorDiagnosticsTruncated: boolean; + #warningDiagnosticsTruncated: boolean; + #errorCount: number; + #warningCount: number; + #logPath: string | undefined; + #logFormat: string | undefined; + #artifactComplete: boolean; + #finalEmitted: boolean; public constructor(options: IAiReporterOptions) { - this._write = options.write; - this._maxBytes = options.maxBytes ?? REPORTER_PERFORMANCE_BUDGETS.maxAiOutputBytes; - this._maxDetailedDiagnostics = + this.#write = options.write; + this.#maxBytes = options.maxBytes ?? REPORTER_PERFORMANCE_BUDGETS.maxAiOutputBytes; + this.#maxDetailedDiagnostics = options.maxDetailedDiagnostics ?? REPORTER_PERFORMANCE_BUDGETS.maxAiDetailedDiagnostics; - if (!Number.isInteger(this._maxBytes) || this._maxBytes < MIN_AI_MAX_BYTES) { + if (!Number.isInteger(this.#maxBytes) || this.#maxBytes < MIN_AI_MAX_BYTES) { throw new RangeError(`maxBytes must be an integer of at least ${MIN_AI_MAX_BYTES}`); } - if (!Number.isInteger(this._maxDetailedDiagnostics) || this._maxDetailedDiagnostics < 0) { + if (!Number.isInteger(this.#maxDetailedDiagnostics) || this.#maxDetailedDiagnostics < 0) { throw new RangeError('maxDetailedDiagnostics must be a nonnegative integer'); } - this._protocolVersion = REPORTER_PROTOCOL_VERSION; - this._commandName = undefined; - this._projectByOperation = new Map(); - this._operationCounts = {}; - this._failedProjects = []; - this._errorDiagnostics = []; - this._warningDiagnostics = []; - this._errorCodes = new Set(); - this._diagnosticCategoryCounts = {}; - this._errorDiagnosticsTruncated = false; - this._warningDiagnosticsTruncated = false; - this._errorCount = 0; - this._warningCount = 0; - this._logPath = undefined; - this._logFormat = undefined; - this._artifactComplete = true; - this._finalEmitted = false; + this.#protocolVersion = REPORTER_PROTOCOL_VERSION; + this.#commandName = undefined; + this.#projectByOperation = new Map(); + this.#operationCounts = {}; + this.#failedProjects = []; + this.#errorDiagnostics = []; + this.#warningDiagnostics = []; + this.#errorCodes = new Set(); + this.#diagnosticCategoryCounts = {}; + this.#errorDiagnosticsTruncated = false; + this.#warningDiagnosticsTruncated = false; + this.#errorCount = 0; + this.#warningCount = 0; + this.#logPath = undefined; + this.#logFormat = undefined; + this.#artifactComplete = true; + this.#finalEmitted = false; } public async initializeAsync(): Promise { @@ -160,15 +160,15 @@ export class AiReporter implements IReporter { } public report(event: IReporterEventEnvelope): void { - this._protocolVersion = event.protocolVersion; + this.#protocolVersion = event.protocolVersion; switch (event.type) { case 'commandStarted': { - this._commandName = (event.payload as { commandName: string }).commandName; - this._write( + this.#commandName = (event.payload as { commandName: string }).commandName; + this.#write( `${JSON.stringify({ kind: 'ai.status', - protocolVersion: this._protocolVersion, - commandName: this._commandName + protocolVersion: this.#protocolVersion, + commandName: this.#commandName })}\n` ); break; @@ -179,7 +179,7 @@ export class AiReporter implements IReporter { projectName?: string; }; if (payload.projectName !== undefined) { - this._projectByOperation.set(payload.operationId, payload.projectName); + this.#projectByOperation.set(payload.operationId, payload.projectName); } break; } @@ -189,28 +189,28 @@ export class AiReporter implements IReporter { status: string; }; if (TERMINAL_STATUSES.has(payload.status)) { - this._operationCounts[payload.status] = (this._operationCounts[payload.status] ?? 0) + 1; + this.#operationCounts[payload.status] = (this.#operationCounts[payload.status] ?? 0) + 1; if (payload.status === 'failure') { const projectName: string = - this._projectByOperation.get(payload.operationId) ?? + this.#projectByOperation.get(payload.operationId) ?? event.scope?.projectName ?? payload.operationId; - this._failedProjects.push(projectName); + this.#failedProjects.push(projectName); } } break; } case 'diagnosticEmitted': { - this._collectDiagnostic(event.payload as IAiDiagnostic); + this.#collectDiagnostic(event.payload as IAiDiagnostic); break; } case 'artifactAvailable': { const payload: { role?: string; path?: string; format?: string; complete?: boolean } = event.payload as { role?: string; path?: string; format?: string; complete?: boolean }; if (payload.role === 'log' && payload.path !== undefined) { - this._logPath = payload.path; - this._logFormat = payload.format; - this._artifactComplete = payload.complete !== false; + this.#logPath = payload.path; + this.#logFormat = payload.format; + this.#artifactComplete = payload.complete !== false; } break; } @@ -219,7 +219,7 @@ export class AiReporter implements IReporter { succeeded: boolean; exitCode: number; }; - this._emitFinal(payload.succeeded, payload.exitCode); + this.#emitFinal(payload.succeeded, payload.exitCode); break; } default: @@ -232,54 +232,54 @@ export class AiReporter implements IReporter { } public async closeAsync(): Promise { - if (!this._finalEmitted) { - this._emitFinal(false, 1); + if (!this.#finalEmitted) { + this.#emitFinal(false, 1); } } - private _collectDiagnostic(diagnostic: IAiDiagnostic): void { + #collectDiagnostic(diagnostic: IAiDiagnostic): void { if (diagnostic.category !== undefined) { - this._diagnosticCategoryCounts[diagnostic.category] = - (this._diagnosticCategoryCounts[diagnostic.category] ?? 0) + 1; + this.#diagnosticCategoryCounts[diagnostic.category] = + (this.#diagnosticCategoryCounts[diagnostic.category] ?? 0) + 1; } if (diagnostic.severity === 'error') { - this._errorCount++; - this._errorCodes.add(diagnostic.code); - if (this._errorDiagnostics.length < this._maxDetailedDiagnostics) { - this._errorDiagnostics.push({ + this.#errorCount++; + this.#errorCodes.add(diagnostic.code); + if (this.#errorDiagnostics.length < this.#maxDetailedDiagnostics) { + this.#errorDiagnostics.push({ code: diagnostic.code, category: diagnostic.category, severity: 'error', remediation: diagnostic.remediation }); } else { - this._errorDiagnosticsTruncated = true; + this.#errorDiagnosticsTruncated = true; } } else if (diagnostic.severity === 'warning') { - this._warningCount++; - if (this._warningDiagnostics.length < this._maxDetailedDiagnostics) { - this._warningDiagnostics.push({ + this.#warningCount++; + if (this.#warningDiagnostics.length < this.#maxDetailedDiagnostics) { + this.#warningDiagnostics.push({ code: diagnostic.code, category: diagnostic.category, severity: 'warning', remediation: diagnostic.remediation }); } else { - this._warningDiagnosticsTruncated = true; + this.#warningDiagnosticsTruncated = true; } } } - private _emitFinal(succeeded: boolean, exitCode: number): void { - if (this._finalEmitted) { + #emitFinal(succeeded: boolean, exitCode: number): void { + if (this.#finalEmitted) { return; } - this._finalEmitted = true; + this.#finalEmitted = true; - const hasFailures: boolean = !succeeded || this._errorCount > 0; + const hasFailures: boolean = !succeeded || this.#errorCount > 0; // When failures exist, warnings are represented by counts only. Warning-only // success may include bounded warning details. - const detailedSource: IAiDiagnostic[] = hasFailures ? this._errorDiagnostics : this._warningDiagnostics; + const detailedSource: IAiDiagnostic[] = hasFailures ? this.#errorDiagnostics : this.#warningDiagnostics; const record: { kind: 'ai.final'; @@ -297,21 +297,21 @@ export class AiReporter implements IReporter { truncated: boolean; } = { kind: 'ai.final', - protocolVersion: this._protocolVersion, + protocolVersion: this.#protocolVersion, result: succeeded ? 'succeeded' : 'failed', exitCode, - scope: { commandName: this._commandName, failedProjects: [...this._failedProjects] }, - errorCodes: [...this._errorCodes].sort(), - diagnosticCategoryCounts: { ...this._diagnosticCategoryCounts }, - diagnostics: detailedSource.slice(0, this._maxDetailedDiagnostics), - errorCount: this._errorCount, - warningCount: this._warningCount, - operationCounts: { ...this._operationCounts }, - truncated: hasFailures ? this._errorDiagnosticsTruncated : this._warningDiagnosticsTruncated + scope: { commandName: this.#commandName, failedProjects: [...this.#failedProjects] }, + errorCodes: [...this.#errorCodes].sort(), + diagnosticCategoryCounts: { ...this.#diagnosticCategoryCounts }, + diagnostics: detailedSource.slice(0, this.#maxDetailedDiagnostics), + errorCount: this.#errorCount, + warningCount: this.#warningCount, + operationCounts: { ...this.#operationCounts }, + truncated: hasFailures ? this.#errorDiagnosticsTruncated : this.#warningDiagnosticsTruncated }; - if (this._logPath !== undefined) { - record.log = { path: this._logPath, format: this._logFormat, complete: this._artifactComplete }; + if (this.#logPath !== undefined) { + record.log = { path: this.#logPath, format: this.#logFormat, complete: this.#artifactComplete }; } // Enforce the byte cap by progressively trimming detailed diagnostics, then @@ -328,17 +328,17 @@ export class AiReporter implements IReporter { } ]; for (const target of trimTargets) { - while (Buffer.byteLength(JSON.stringify(record), 'utf8') > this._maxBytes && target.get().length > 0) { + while (Buffer.byteLength(JSON.stringify(record), 'utf8') > this.#maxBytes && target.get().length > 0) { target.set(target.get().slice(0, target.get().length - 1)); record.truncated = true; } - if (Buffer.byteLength(JSON.stringify(record), 'utf8') <= this._maxBytes) { + if (Buffer.byteLength(JSON.stringify(record), 'utf8') <= this.#maxBytes) { break; } } let serialized: string = JSON.stringify(record); - if (Buffer.byteLength(serialized, 'utf8') > this._maxBytes) { + if (Buffer.byteLength(serialized, 'utf8') > this.#maxBytes) { record.scope = { failedProjects: [] }; record.errorCodes = []; record.diagnosticCategoryCounts = {}; @@ -348,9 +348,9 @@ export class AiReporter implements IReporter { record.truncated = true; serialized = JSON.stringify(record); } - if (Buffer.byteLength(serialized, 'utf8') > this._maxBytes) { - throw new Error(`The minimal AI final record exceeds maxBytes=${this._maxBytes}`); + if (Buffer.byteLength(serialized, 'utf8') > this.#maxBytes) { + throw new Error(`The minimal AI final record exceeds maxBytes=${this.#maxBytes}`); } - this._write(`${serialized}\n`); + this.#write(`${serialized}\n`); } } diff --git a/libraries/reporter/src/reporters/DefaultInteractiveReporter.ts b/libraries/reporter/src/reporters/DefaultInteractiveReporter.ts index 708322f381b..8230dc0c33e 100644 --- a/libraries/reporter/src/reporters/DefaultInteractiveReporter.ts +++ b/libraries/reporter/src/reporters/DefaultInteractiveReporter.ts @@ -103,53 +103,53 @@ export interface IDefaultInteractiveReporterOptions { export class DefaultInteractiveReporter implements IReporter { public readonly name: string = 'default'; - private readonly _terminal: IInteractiveTerminal; - private readonly _color: IColorizer; - private readonly _colorEnabled: boolean; - private readonly _nowMs: () => number; - private readonly _minRefreshIntervalMs: number; + readonly #terminal: IInteractiveTerminal; + readonly #color: IColorizer; + readonly #colorEnabled: boolean; + readonly #nowMs: () => number; + readonly #minRefreshIntervalMs: number; - private _commandName: string | undefined; - private _totalOperations: number; - private _completedOperations: number; - private _failedOperations: number; - private readonly _projectByOperation: Map; - private readonly _activeProjects: Map; - private _latestActivity: string; - private readonly _diagnostics: string[]; - private _result: { succeeded: boolean; exitCode: number } | undefined; - private _logPath: string | undefined; + #commandName: string | undefined; + #totalOperations: number; + #completedOperations: number; + #failedOperations: number; + readonly #projectByOperation: Map; + readonly #activeProjects: Map; + #latestActivity: string; + readonly #diagnostics: string[]; + #result: { succeeded: boolean; exitCode: number } | undefined; + #logPath: string | undefined; - private _spinnerIndex: number; - private _lastPaintMs: number; - private _paintedRowCount: number; - private _cursorHidden: boolean; - private _finalized: boolean; + #spinnerIndex: number; + #lastPaintMs: number; + #paintedRowCount: number; + #cursorHidden: boolean; + #finalized: boolean; public constructor(options: IDefaultInteractiveReporterOptions) { - this._terminal = options.terminal; - this._colorEnabled = + this.#terminal = options.terminal; + this.#colorEnabled = options.color ?? resolveColorEnabled(options.env ?? process.env, options.terminal.isTTY); - this._color = createColorizer(this._colorEnabled); - this._nowMs = options.nowMs ?? (() => Date.now()); - this._minRefreshIntervalMs = options.minRefreshIntervalMs ?? MIN_REFRESH_INTERVAL_MS; + this.#color = createColorizer(this.#colorEnabled); + this.#nowMs = options.nowMs ?? (() => Date.now()); + this.#minRefreshIntervalMs = options.minRefreshIntervalMs ?? MIN_REFRESH_INTERVAL_MS; - this._commandName = undefined; - this._totalOperations = 0; - this._completedOperations = 0; - this._failedOperations = 0; - this._projectByOperation = new Map(); - this._activeProjects = new Map(); - this._latestActivity = ''; - this._diagnostics = []; - this._result = undefined; - this._logPath = options.logPath; + this.#commandName = undefined; + this.#totalOperations = 0; + this.#completedOperations = 0; + this.#failedOperations = 0; + this.#projectByOperation = new Map(); + this.#activeProjects = new Map(); + this.#latestActivity = ''; + this.#diagnostics = []; + this.#result = undefined; + this.#logPath = options.logPath; - this._spinnerIndex = 0; - this._lastPaintMs = Number.NEGATIVE_INFINITY; - this._paintedRowCount = 0; - this._cursorHidden = false; - this._finalized = false; + this.#spinnerIndex = 0; + this.#lastPaintMs = Number.NEGATIVE_INFINITY; + this.#paintedRowCount = 0; + this.#cursorHidden = false; + this.#finalized = false; } public async initializeAsync(): Promise { @@ -157,30 +157,30 @@ export class DefaultInteractiveReporter implements IReporter { } public report(event: IReporterEventEnvelope): void { - this._update(event); + this.#update(event); if (event.type === 'watchCycleCompleted') { - this._appendWatchSummary(event); + this.#appendWatchSummary(event); return; } - if (this._terminal.isTTY && shouldRefresh(this._lastPaintMs, this._nowMs(), this._minRefreshIntervalMs)) { - this._paint(); + if (this.#terminal.isTTY && shouldRefresh(this.#lastPaintMs, this.#nowMs(), this.#minRefreshIntervalMs)) { + this.#paint(); } } public async flushAsync(): Promise { - if (this._terminal.isTTY && !this._finalized) { - this._paint(); + if (this.#terminal.isTTY && !this.#finalized) { + this.#paint(); } } public async closeAsync(): Promise { - this._finalize(); + this.#finalize(); } - private _update(event: IReporterEventEnvelope): void { + #update(event: IReporterEventEnvelope): void { switch (event.type) { case 'commandStarted': { - this._commandName = (event.payload as { commandName?: string }).commandName; + this.#commandName = (event.payload as { commandName?: string }).commandName; break; } case 'operationRegistered': { @@ -188,8 +188,8 @@ export class DefaultInteractiveReporter implements IReporter { operationId: string; projectName?: string; }; - this._totalOperations++; - this._projectByOperation.set( + this.#totalOperations++; + this.#projectByOperation.set( payload.operationId, payload.projectName ?? event.scope?.projectName ?? payload.operationId ); @@ -204,24 +204,24 @@ export class DefaultInteractiveReporter implements IReporter { const projectName: string = payload.projectName ?? event.scope?.projectName ?? - this._projectByOperation.get(payload.operationId) ?? + this.#projectByOperation.get(payload.operationId) ?? payload.operationId; if (payload.status === 'executing') { - this._activeProjects.set(payload.operationId, projectName); + this.#activeProjects.set(payload.operationId, projectName); } else if (TERMINAL_STATUSES.has(payload.status)) { - this._activeProjects.delete(payload.operationId); - this._completedOperations++; + this.#activeProjects.delete(payload.operationId); + this.#completedOperations++; if (payload.status === 'failure') { - this._failedOperations++; + this.#failedOperations++; } } - this._latestActivity = `${payload.status} ${projectName}`; + this.#latestActivity = `${payload.status} ${projectName}`; break; } case 'activityChanged': { const payload: { kind?: string; text?: string } = event.payload as { kind?: string; text?: string }; if (payload.text !== undefined) { - this._latestActivity = payload.text; + this.#latestActivity = payload.text; } break; } @@ -231,19 +231,19 @@ export class DefaultInteractiveReporter implements IReporter { severity?: string; }; if (payload.severity === 'error' || payload.severity === 'warning') { - this._diagnostics.push(`[${payload.severity}] ${payload.code ?? 'unknown'}`); + this.#diagnostics.push(`[${payload.severity}] ${payload.code ?? 'unknown'}`); } break; } case 'artifactAvailable': { const payload: { role?: string; path?: string } = event.payload as { role?: string; path?: string }; if (payload.role === 'log' && payload.path !== undefined) { - this._logPath = payload.path; + this.#logPath = payload.path; } break; } case 'commandResult': { - this._result = event.payload as { succeeded: boolean; exitCode: number }; + this.#result = event.payload as { succeeded: boolean; exitCode: number }; break; } default: @@ -251,84 +251,84 @@ export class DefaultInteractiveReporter implements IReporter { } } - private _snapshot(): ILiveRegionState { + #snapshot(): ILiveRegionState { return { - commandName: this._commandName, - totalOperations: this._totalOperations, - completedOperations: this._completedOperations, - failedOperations: this._failedOperations, - activeProjects: [...this._activeProjects.values()], - latestActivity: this._latestActivity + commandName: this.#commandName, + totalOperations: this.#totalOperations, + completedOperations: this.#completedOperations, + failedOperations: this.#failedOperations, + activeProjects: [...this.#activeProjects.values()], + latestActivity: this.#latestActivity }; } - private _paint(): void { - if (!this._cursorHidden) { - this._terminal.write(HIDE_CURSOR); - this._cursorHidden = true; + #paint(): void { + if (!this.#cursorHidden) { + this.#terminal.write(HIDE_CURSOR); + this.#cursorHidden = true; } - const spinnerFrame: string = SPINNER_FRAMES[this._spinnerIndex % SPINNER_FRAMES.length]; - this._spinnerIndex++; - const rows: string[] = renderLiveRegion(this._snapshot(), { - width: this._terminal.columns, + const spinnerFrame: string = SPINNER_FRAMES[this.#spinnerIndex % SPINNER_FRAMES.length]; + this.#spinnerIndex++; + const rows: string[] = renderLiveRegion(this.#snapshot(), { + width: this.#terminal.columns, spinnerFrame, - color: this._color + color: this.#color }); - this._terminal.write(`${this._clearRegion()}${rows.join('\n')}\n`); - this._paintedRowCount = rows.length; - this._lastPaintMs = this._nowMs(); + this.#terminal.write(`${this.#clearRegion()}${rows.join('\n')}\n`); + this.#paintedRowCount = rows.length; + this.#lastPaintMs = this.#nowMs(); } - private _clearRegion(): string { - if (this._paintedRowCount === 0) { + #clearRegion(): string { + if (this.#paintedRowCount === 0) { return ''; } - return `\u001b[${this._paintedRowCount}A\u001b[0J`; + return `\u001b[${this.#paintedRowCount}A\u001b[0J`; } - private _appendWatchSummary(event: IReporterEventEnvelope): void { + #appendWatchSummary(event: IReporterEventEnvelope): void { const payload: { succeeded?: boolean } = event.payload as { succeeded?: boolean }; - const marker: string = payload.succeeded ? this._color.green('✔') : this._color.red('✖'); + const marker: string = payload.succeeded ? this.#color.green('✔') : this.#color.red('✖'); const summary: string = `${marker} watch cycle ${payload.succeeded ? 'succeeded' : 'failed'}`; - this._terminal.write(`${this._clearRegion()}${summary}\n`); - this._paintedRowCount = 0; - if (this._terminal.isTTY) { - this._paint(); + this.#terminal.write(`${this.#clearRegion()}${summary}\n`); + this.#paintedRowCount = 0; + if (this.#terminal.isTTY) { + this.#paint(); } } - private _finalize(): void { - if (this._finalized) { + #finalize(): void { + if (this.#finalized) { return; } - this._finalized = true; + this.#finalized = true; const lines: string[] = []; - const succeeded: boolean = this._result?.succeeded ?? false; + const succeeded: boolean = this.#result?.succeeded ?? false; if (succeeded) { lines.push( - `${this._color.green('✔')} ${this._commandName ?? 'rush'} succeeded — ` + - `${this._completedOperations}/${this._totalOperations} operations` + `${this.#color.green('✔')} ${this.#commandName ?? 'rush'} succeeded — ` + + `${this.#completedOperations}/${this.#totalOperations} operations` ); } else { lines.push( - `${this._color.red('✖')} ${this._commandName ?? 'rush'} failed — ${this._failedOperations} failed` + `${this.#color.red('✖')} ${this.#commandName ?? 'rush'} failed — ${this.#failedOperations} failed` ); - for (const diagnostic of this._diagnostics.slice(0, MAX_FINAL_DIAGNOSTICS)) { + for (const diagnostic of this.#diagnostics.slice(0, MAX_FINAL_DIAGNOSTICS)) { lines.push(` ${diagnostic}`); } - if (this._diagnostics.length > MAX_FINAL_DIAGNOSTICS) { - lines.push(` +${this._diagnostics.length - MAX_FINAL_DIAGNOSTICS} more diagnostics`); + if (this.#diagnostics.length > MAX_FINAL_DIAGNOSTICS) { + lines.push(` +${this.#diagnostics.length - MAX_FINAL_DIAGNOSTICS} more diagnostics`); } - if (this._logPath !== undefined) { - lines.push(` ${this._color.dim(`Log: ${this._logPath}`)}`); + if (this.#logPath !== undefined) { + lines.push(` ${this.#color.dim(`Log: ${this.#logPath}`)}`); } } - const clear: string = this._clearRegion(); - const restore: string = this._cursorHidden ? SHOW_CURSOR : ''; - this._cursorHidden = false; - this._paintedRowCount = 0; - this._terminal.write(`${clear}${lines.join('\n')}\n${restore}`); + const clear: string = this.#clearRegion(); + const restore: string = this.#cursorHidden ? SHOW_CURSOR : ''; + this.#cursorHidden = false; + this.#paintedRowCount = 0; + this.#terminal.write(`${clear}${lines.join('\n')}\n${restore}`); } } diff --git a/libraries/reporter/src/reporters/FileReporter.ts b/libraries/reporter/src/reporters/FileReporter.ts index 017d85e9a4c..3c8ce80c9b4 100644 --- a/libraries/reporter/src/reporters/FileReporter.ts +++ b/libraries/reporter/src/reporters/FileReporter.ts @@ -117,177 +117,179 @@ export interface IFileReporterOptions { export class FileReporter implements IReporter { public readonly name: string = 'file'; - private readonly _commonTempFolder: string | undefined; - private readonly _osTempFolder: string; - private readonly _actionName: string; - private readonly _pid: number; - private readonly _nowMs: () => number; - private readonly _retentionDays: number; - private readonly _maxSessions: number; - private readonly _emergencyWarn: (message: string) => void; - - private readonly _lines: string[]; - private _fileDescriptor: number | undefined; - private _targetResolved: boolean; - private _available: boolean; - private _targetPath: string | undefined; - private _latestCopyPath: string | undefined; - private readonly _fileName: string; + readonly #commonTempFolder: string | undefined; + readonly #osTempFolder: string; + readonly #actionName: string; + readonly #pid: number; + readonly #nowMs: () => number; + readonly #retentionDays: number; + readonly #maxSessions: number; + readonly #emergencyWarn: (message: string) => void; + + readonly #lines: string[]; + #fileDescriptor: number | undefined; + #targetResolved: boolean; + #available: boolean; + #targetPath: string | undefined; + #latestCopyPath: string | undefined; + readonly #fileName: string; public constructor(options: IFileReporterOptions = {}) { - this._commonTempFolder = options.commonTempFolder; - this._osTempFolder = options.osTempFolder ?? os.tmpdir(); - this._actionName = options.actionName ?? 'rush'; - this._pid = options.pid ?? process.pid; - this._nowMs = options.nowMs ?? (() => Date.now()); - this._retentionDays = options.retentionDays ?? DEFAULT_RETENTION_DAYS; - this._maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS; - this._emergencyWarn = + this.#commonTempFolder = options.commonTempFolder; + this.#osTempFolder = options.osTempFolder ?? os.tmpdir(); + this.#actionName = options.actionName ?? 'rush'; + this.#pid = options.pid ?? process.pid; + this.#nowMs = options.nowMs ?? (() => Date.now()); + this.#retentionDays = options.retentionDays ?? DEFAULT_RETENTION_DAYS; + this.#maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS; + this.#emergencyWarn = options.emergencyWarn ?? ((message: string) => { process.stderr.write(`${message}\n`); }); - this._lines = []; - this._fileDescriptor = undefined; - this._targetResolved = false; - this._available = false; - this._targetPath = undefined; - this._latestCopyPath = undefined; + this.#lines = []; + this.#fileDescriptor = undefined; + this.#targetResolved = false; + this.#available = false; + this.#targetPath = undefined; + this.#latestCopyPath = undefined; - const timestamp: string = new Date(this._nowMs()).toISOString().replace(/[:.]/g, '-'); - this._fileName = `${timestamp}-${this._pid}-${this._actionName}.log`; + const timestamp: string = new Date(this.#nowMs()).toISOString().replace(/[:.]/g, '-'); + this.#fileName = `${timestamp}-${this.#pid}-${this.#actionName}.log`; } public async initializeAsync(): Promise { - await this._ensureTargetAsync(); - this._writeBufferedLines(); + await this.#ensureTargetAsync(); + this.#writeBufferedLines(); } public report(event: IReporterEventEnvelope): void { - const line: string = this._formatLine(event); - if (this._fileDescriptor === undefined) { - if (!this._targetResolved) { - this._lines.push(line); + const line: string = this.#formatLine(event); + if (this.#fileDescriptor === undefined) { + if (!this.#targetResolved) { + this.#lines.push(line); } return; } - this._writeLine(line); + this.#writeLine(line); } public async flushAsync(): Promise { - await this._ensureTargetAsync(); - this._writeBufferedLines(); - if (this._fileDescriptor !== undefined) { + await this.#ensureTargetAsync(); + this.#writeBufferedLines(); + if (this.#fileDescriptor !== undefined) { try { - fs.fsyncSync(this._fileDescriptor); + fs.fsyncSync(this.#fileDescriptor); } catch (error) { - this._markUnavailable(error as Error); + this.#markUnavailable(error as Error); } } - await this._refreshLatestCopyAsync(); + await this.#refreshLatestCopyAsync(); } public async closeAsync(): Promise { await this.flushAsync(); - if (this._fileDescriptor !== undefined) { + if (this.#fileDescriptor !== undefined) { try { - fs.closeSync(this._fileDescriptor); + fs.closeSync(this.#fileDescriptor); } catch (error) { - this._available = false; - this._emergencyWarn( - `[reporter] Unable to close the full-detail log; the artifact is unavailable: ${(error as Error).message}` + this.#available = false; + this.#emergencyWarn( + `[reporter] Unable to close the full-detail log; the artifact is unavailable: ${ + (error as Error).message + }` ); } finally { - this._fileDescriptor = undefined; + this.#fileDescriptor = undefined; } } - await this._refreshLatestCopyAsync(); + await this.#refreshLatestCopyAsync(); } /** * Returns the resolved log artifact. */ public getArtifact(): IFileReporterArtifact { - return this._targetPath !== undefined - ? { available: this._available, path: this._targetPath } - : { available: this._available }; + return this.#targetPath !== undefined + ? { available: this.#available, path: this.#targetPath } + : { available: this.#available }; } - private _formatLine(event: IReporterEventEnvelope): string { + #formatLine(event: IReporterEventEnvelope): string { return `${JSON.stringify(redactReporterEvent(event))}\n`; } - private async _ensureTargetAsync(): Promise { - if (!this._targetResolved) { - this._targetResolved = true; - await this._resolveTargetAsync(); + async #ensureTargetAsync(): Promise { + if (!this.#targetResolved) { + this.#targetResolved = true; + await this.#resolveTargetAsync(); } } - private _writeBufferedLines(): void { - if (this._fileDescriptor === undefined) { - this._lines.length = 0; + #writeBufferedLines(): void { + if (this.#fileDescriptor === undefined) { + this.#lines.length = 0; return; } - const newLines: string[] = this._lines.splice(0); + const newLines: string[] = this.#lines.splice(0); for (const line of newLines) { - if (!this._writeLine(line)) { + if (!this.#writeLine(line)) { break; } } } - private _writeLine(line: string): boolean { - if (this._fileDescriptor === undefined) { + #writeLine(line: string): boolean { + if (this.#fileDescriptor === undefined) { return false; } try { - fs.writeSync(this._fileDescriptor, line, null, 'utf8'); + fs.writeSync(this.#fileDescriptor, line, null, 'utf8'); return true; } catch (error) { - this._markUnavailable(error as Error); + this.#markUnavailable(error as Error); return false; } } - private async _refreshLatestCopyAsync(): Promise { - if (this._latestCopyPath === undefined || this._targetPath === undefined || !this._available) { + async #refreshLatestCopyAsync(): Promise { + if (this.#latestCopyPath === undefined || this.#targetPath === undefined || !this.#available) { return; } try { - await fs.promises.copyFile(this._targetPath, this._latestCopyPath); + await fs.promises.copyFile(this.#targetPath, this.#latestCopyPath); } catch { /* latest.log is best-effort. */ } } - private _markUnavailable(error: Error): void { - if (!this._available) { + #markUnavailable(error: Error): void { + if (!this.#available) { return; } - this._available = false; - this._lines.length = 0; - if (this._fileDescriptor !== undefined) { + this.#available = false; + this.#lines.length = 0; + if (this.#fileDescriptor !== undefined) { try { - fs.closeSync(this._fileDescriptor); + fs.closeSync(this.#fileDescriptor); } catch { /* The original write failure is more useful. */ } - this._fileDescriptor = undefined; + this.#fileDescriptor = undefined; } - this._emergencyWarn( + this.#emergencyWarn( `[reporter] Unable to write the full-detail log; the artifact is unavailable: ${error.message}` ); } - private async _resolveTargetAsync(): Promise { + async #resolveTargetAsync(): Promise { const candidateDirs: Array<{ path: string; ownerOnly: boolean }> = []; - if (this._commonTempFolder !== undefined) { - candidateDirs.push({ path: path.join(this._commonTempFolder, RUSH_LOGS_DIR_NAME), ownerOnly: false }); + if (this.#commonTempFolder !== undefined) { + candidateDirs.push({ path: path.join(this.#commonTempFolder, RUSH_LOGS_DIR_NAME), ownerOnly: false }); } candidateDirs.push({ - path: path.join(this._osTempFolder, getUserTempDirectoryName()), + path: path.join(this.#osTempFolder, getUserTempDirectoryName()), ownerOnly: true }); @@ -302,40 +304,42 @@ export class FileReporter implements IReporter { if (candidate.ownerOnly) { await fs.promises.chmod(dir, OWNER_ONLY_DIRECTORY_MODE); } - const filePath: string = path.join(dir, this._fileName); + const filePath: string = path.join(dir, this.#fileName); await fs.promises.writeFile(filePath, '', { mode: OWNER_ONLY_MODE }); await fs.promises.chmod(filePath, OWNER_ONLY_MODE); const fileDescriptor: number = fs.openSync(filePath, 'a'); - this._fileDescriptor = fileDescriptor; - this._targetPath = filePath; - this._available = true; - await this._updateLatestAsync(dir, filePath); - await this._applyRetentionAsync(dir); + this.#fileDescriptor = fileDescriptor; + this.#targetPath = filePath; + this.#available = true; + await this.#updateLatestAsync(dir, filePath); + await this.#applyRetentionAsync(dir); return; } catch (error) { lastError = error as Error; } } - this._available = false; - this._lines.length = 0; - this._emergencyWarn( - `[reporter] Unable to write the full-detail log; the artifact is unavailable: ${lastError?.message ?? 'unknown error'}` + this.#available = false; + this.#lines.length = 0; + this.#emergencyWarn( + `[reporter] Unable to write the full-detail log; the artifact is unavailable: ${ + lastError?.message ?? 'unknown error' + }` ); } - private async _updateLatestAsync(dir: string, filePath: string): Promise { + async #updateLatestAsync(dir: string, filePath: string): Promise { const latestPath: string = path.join(dir, LATEST_LOG_NAME); try { await fs.promises.rm(latestPath, { force: true }); await fs.promises.symlink(path.basename(filePath), latestPath); - this._latestCopyPath = undefined; + this.#latestCopyPath = undefined; } catch { - this._latestCopyPath = latestPath; + this.#latestCopyPath = latestPath; } } - private async _applyRetentionAsync(dir: string): Promise { + async #applyRetentionAsync(dir: string): Promise { let entries: string[]; try { entries = await fs.promises.readdir(dir); @@ -343,7 +347,7 @@ export class FileReporter implements IReporter { return; } - const cutoff: number = this._nowMs() - this._retentionDays * MS_PER_DAY; + const cutoff: number = this.#nowMs() - this.#retentionDays * MS_PER_DAY; const logs: { path: string; mtimeMs: number }[] = []; for (const entry of entries) { if (entry === LATEST_LOG_NAME || !entry.endsWith('.log')) { @@ -362,9 +366,9 @@ export class FileReporter implements IReporter { } } - if (logs.length > this._maxSessions) { + if (logs.length > this.#maxSessions) { logs.sort((a, b) => a.mtimeMs - b.mtimeMs); - const excess: number = logs.length - this._maxSessions; + const excess: number = logs.length - this.#maxSessions; for (let index: number = 0; index < excess; index++) { try { await fs.promises.rm(logs[index].path, { force: true }); diff --git a/libraries/reporter/src/reporters/JsonReporter.ts b/libraries/reporter/src/reporters/JsonReporter.ts index 55d08ffdfb8..2e602e68fba 100644 --- a/libraries/reporter/src/reporters/JsonReporter.ts +++ b/libraries/reporter/src/reporters/JsonReporter.ts @@ -37,12 +37,12 @@ export interface IJsonReporterOptions { export class JsonReporter implements IReporter { public readonly name: string = 'json'; - private readonly _write: (text: string) => void; - private readonly _maxRecordBytes: number; + readonly #write: (text: string) => void; + readonly #maxRecordBytes: number; public constructor(options: IJsonReporterOptions) { - this._write = options.write; - this._maxRecordBytes = options.maxRecordBytes ?? REPORTER_PROTOCOL_LIMITS.ndjsonRecordBytes; + this.#write = options.write; + this.#maxRecordBytes = options.maxRecordBytes ?? REPORTER_PROTOCOL_LIMITS.ndjsonRecordBytes; } public async initializeAsync(): Promise { @@ -51,12 +51,10 @@ export class JsonReporter implements IReporter { public report(event: IReporterEventEnvelope): void { try { - this._write( - encodeNdjsonRecord(redactReporterEvent(event), { maxRecordBytes: this._maxRecordBytes }) - ); + this.#write(encodeNdjsonRecord(redactReporterEvent(event), { maxRecordBytes: this.#maxRecordBytes })); } catch (error) { if (error instanceof NdjsonRecordTooLargeError) { - this._write( + this.#write( encodeNdjsonRecord( { ...event, @@ -67,7 +65,7 @@ export class JsonReporter implements IReporter { payload: { originalType: event.type } } }, - { maxRecordBytes: this._maxRecordBytes } + { maxRecordBytes: this.#maxRecordBytes } ) ); return; diff --git a/libraries/reporter/src/reporters/LegacyReporter.ts b/libraries/reporter/src/reporters/LegacyReporter.ts index 743b33cf387..8a14e25e579 100644 --- a/libraries/reporter/src/reporters/LegacyReporter.ts +++ b/libraries/reporter/src/reporters/LegacyReporter.ts @@ -75,28 +75,28 @@ export interface ILegacyReporterOptions { export class LegacyReporter implements IReporter { public readonly name: string = 'legacy'; - private readonly _write: (text: string) => void; - private readonly _maxParallelism: number | undefined; + readonly #write: (text: string) => void; + readonly #maxParallelism: number | undefined; - private _commandName: string | undefined; - private _total: number; - private _ordinal: number; - private _totalDurationMs: number; - private readonly _registry: Map; - private readonly _outputBuffers: Map; - private readonly _recordsByStatus: Map; + #commandName: string | undefined; + #total: number; + #ordinal: number; + #totalDurationMs: number; + readonly #registry: Map; + readonly #outputBuffers: Map; + readonly #recordsByStatus: Map; public constructor(options: ILegacyReporterOptions) { - this._write = options.write; - this._maxParallelism = options.maxParallelism; - - this._commandName = undefined; - this._total = 0; - this._ordinal = 0; - this._totalDurationMs = 0; - this._registry = new Map(); - this._outputBuffers = new Map(); - this._recordsByStatus = new Map(); + this.#write = options.write; + this.#maxParallelism = options.maxParallelism; + + this.#commandName = undefined; + this.#total = 0; + this.#ordinal = 0; + this.#totalDurationMs = 0; + this.#registry = new Map(); + this.#outputBuffers = new Map(); + this.#recordsByStatus = new Map(); } public async initializeAsync(): Promise { @@ -106,10 +106,10 @@ export class LegacyReporter implements IReporter { public report(event: IReporterEventEnvelope): void { switch (event.type) { case 'commandStarted': { - this._commandName = (event.payload as { commandName: string }).commandName; - this._write(`Starting "rush ${this._commandName}"\n\n`); - if (this._maxParallelism !== undefined) { - this._write(`Executing a maximum of ${this._maxParallelism} simultaneous processes...\n`); + this.#commandName = (event.payload as { commandName: string }).commandName; + this.#write(`Starting "rush ${this.#commandName}"\n\n`); + if (this.#maxParallelism !== undefined) { + this.#write(`Executing a maximum of ${this.#maxParallelism} simultaneous processes...\n`); } break; } @@ -119,36 +119,36 @@ export class LegacyReporter implements IReporter { projectName?: string; phaseName?: string; }; - this._registry.set(payload.operationId, this._title(payload.projectName, payload.phaseName)); - this._outputBuffers.set(payload.operationId, []); - this._total++; + this.#registry.set(payload.operationId, this.#title(payload.projectName, payload.phaseName)); + this.#outputBuffers.set(payload.operationId, []); + this.#total++; break; } case 'operationStatusChanged': { - this._onStatusChanged(event); + this.#onStatusChanged(event); break; } case 'externalOutput': { const text: string = (event.payload as { text?: string }).text ?? ''; const operationId: string | undefined = event.scope?.operationId; const buffer: string[] | undefined = - operationId === undefined ? undefined : this._outputBuffers.get(operationId); + operationId === undefined ? undefined : this.#outputBuffers.get(operationId); if (buffer) { buffer.push(text); } else { - this._write(text); + this.#write(text); } break; } case 'commandCompleted': { const durationMs: number | undefined = (event.payload as { durationMs?: number }).durationMs; if (durationMs !== undefined) { - this._totalDurationMs = durationMs; + this.#totalDurationMs = durationMs; } break; } case 'commandResult': { - this._onResult(event.payload as { succeeded: boolean }); + this.#onResult(event.payload as { succeeded: boolean }); break; } default: @@ -164,90 +164,89 @@ export class LegacyReporter implements IReporter { /* no-op */ } - private _onStatusChanged(event: IReporterEventEnvelope): void { + #onStatusChanged(event: IReporterEventEnvelope): void { const payload: { operationId: string; status: string; durationMs?: number } = event.payload as { operationId: string; status: string; durationMs?: number; }; - const title: string = this._registry.get(payload.operationId) ?? payload.operationId; + const title: string = this.#registry.get(payload.operationId) ?? payload.operationId; if (TERMINAL_STATUSES.has(payload.status)) { - this._ordinal++; - this._write(`\n${this._header(title, this._ordinal, this._total)}\n`); - const output: string = this._outputBuffers.get(payload.operationId)?.join('') ?? ''; - this._write(output); + this.#ordinal++; + this.#write(`\n${this.#header(title, this.#ordinal, this.#total)}\n`); + const output: string = this.#outputBuffers.get(payload.operationId)?.join('') ?? ''; + this.#write(output); if (output.length > 0 && !output.endsWith('\n')) { - this._write('\n'); + this.#write('\n'); } - this._outputBuffers.delete(payload.operationId); + this.#outputBuffers.delete(payload.operationId); const record: ILegacyOperationRecord = { title, durationMs: payload.durationMs ?? 0, status: payload.status }; - const records: ILegacyOperationRecord[] = this._recordsByStatus.get(payload.status) ?? []; + const records: ILegacyOperationRecord[] = this.#recordsByStatus.get(payload.status) ?? []; records.push(record); - this._recordsByStatus.set(payload.status, records); + this.#recordsByStatus.set(payload.status, records); } } - private _onResult(payload: { succeeded: boolean }): void { - const commandName: string = this._commandName ?? 'rush'; + #onResult(payload: { succeeded: boolean }): void { + const commandName: string = this.#commandName ?? 'rush'; if (payload.succeeded) { const count: number = - (this._recordsByStatus.get('success')?.length ?? 0) + - (this._recordsByStatus.get('successWithWarnings')?.length ?? 0); - this._write(`\n\n${this._summaryHeader(`SUCCESS: ${count} operations`)}\n\n`); + (this.#recordsByStatus.get('success')?.length ?? 0) + + (this.#recordsByStatus.get('successWithWarnings')?.length ?? 0); + this.#write(`\n\n${this.#summaryHeader(`SUCCESS: ${count} operations`)}\n\n`); } else { - const count: number = this._recordsByStatus.get('failure')?.length ?? 0; - this._write(`\n\n${this._summaryHeader(`FAILURE: ${count} operation`)}\n\n`); + const count: number = this.#recordsByStatus.get('failure')?.length ?? 0; + this.#write(`\n\n${this.#summaryHeader(`FAILURE: ${count} operation`)}\n\n`); } - this._writeStatusGroup('skipped', 'These operations were already up to date:'); - this._writeStatusGroup('noOp', 'These operations did not define any work:'); - this._writeStatusGroup('fromCache', 'These operations were restored from the build cache:'); - this._writeStatusGroup('success', 'These operations completed successfully:'); - this._writeStatusGroup('successWithWarnings', 'These operations succeeded with warnings:'); - this._writeStatusGroup('blocked', 'These operations were blocked by dependencies that failed:'); - this._writeStatusGroup('failure', 'The following projects failed to build:'); + this.#writeStatusGroup('skipped', 'These operations were already up to date:'); + this.#writeStatusGroup('noOp', 'These operations did not define any work:'); + this.#writeStatusGroup('fromCache', 'These operations were restored from the build cache:'); + this.#writeStatusGroup('success', 'These operations completed successfully:'); + this.#writeStatusGroup('successWithWarnings', 'These operations succeeded with warnings:'); + this.#writeStatusGroup('blocked', 'These operations were blocked by dependencies that failed:'); + this.#writeStatusGroup('failure', 'The following projects failed to build:'); const suffix: string = payload.succeeded ? '' : ' ==> ERROR: Project(s) failed to build'; - this._write(`rush ${commandName} (${this._seconds(this._totalDurationMs)} seconds)${suffix}\n`); + this.#write(`rush ${commandName} (${this.#seconds(this.#totalDurationMs)} seconds)${suffix}\n`); } - private _writeStatusGroup(status: string, heading: string): void { - const records: readonly ILegacyOperationRecord[] | undefined = this._recordsByStatus.get(status); + #writeStatusGroup(status: string, heading: string): void { + const records: readonly ILegacyOperationRecord[] | undefined = this.#recordsByStatus.get(status); if (!records || records.length === 0) { return; } - this._write(`${heading}\n`); + this.#write(`${heading}\n`); for (const record of records) { - this._write(` ${record.title} ${this._seconds(record.durationMs)} seconds\n`); + this.#write(` ${record.title} ${this.#seconds(record.durationMs)} seconds\n`); } - this._write('\n'); + this.#write('\n'); } - private _title(projectName: string | undefined, phaseName: string | undefined): string { + #title(projectName: string | undefined, phaseName: string | undefined): string { const project: string = projectName ?? 'unknown'; return phaseName ? `${project} (${phaseName})` : project; } - private _header(title: string, ordinal: number, total: number): string { + #header(title: string, ordinal: number, total: number): string { const left: string = `==[ ${title} ]`; const right: string = `[ ${ordinal} of ${total} ]==`; const fill: number = Math.max(2, HEADER_WIDTH - left.length - right.length); return `${left}${'='.repeat(fill)}${right}`; } - private _summaryHeader(label: string): string { + #summaryHeader(label: string): string { const left: string = `==[ ${label} ]`; const fill: number = Math.max(2, HEADER_WIDTH - left.length); return `${left}${'='.repeat(fill)}`; } - private _seconds(durationMs: number): string { + #seconds(durationMs: number): string { return (durationMs / 1000).toFixed(2); } - } diff --git a/libraries/reporter/src/reporters/PlaintextReporter.ts b/libraries/reporter/src/reporters/PlaintextReporter.ts index 0508f15bcce..087492d1009 100644 --- a/libraries/reporter/src/reporters/PlaintextReporter.ts +++ b/libraries/reporter/src/reporters/PlaintextReporter.ts @@ -71,34 +71,34 @@ export interface IPlaintextReporterOptions { export class PlaintextReporter implements IReporter { public readonly name: string = 'plaintext'; - private readonly _write: (text: string) => void; - private readonly _variant: PlaintextVariant; - private readonly _color: IColorizer; - private readonly _nowMs: () => number; - private readonly _heartbeatIntervalMs: number; + readonly #write: (text: string) => void; + readonly #variant: PlaintextVariant; + readonly #color: IColorizer; + readonly #nowMs: () => number; + readonly #heartbeatIntervalMs: number; - private _commandName: string | undefined; - private _total: number; - private _completed: number; - private _failed: number; - private _lastOutputMs: number; - private _atLineStart: boolean; - private readonly _operations: Map; + #commandName: string | undefined; + #total: number; + #completed: number; + #failed: number; + #lastOutputMs: number; + #atLineStart: boolean; + readonly #operations: Map; public constructor(options: IPlaintextReporterOptions) { - this._write = options.write; - this._variant = options.variant ?? 'concise'; - this._color = createColorizer(options.color ?? false); - this._nowMs = options.nowMs ?? (() => Date.now()); - this._heartbeatIntervalMs = options.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS; + this.#write = options.write; + this.#variant = options.variant ?? 'concise'; + this.#color = createColorizer(options.color ?? false); + this.#nowMs = options.nowMs ?? (() => Date.now()); + this.#heartbeatIntervalMs = options.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS; - this._commandName = undefined; - this._total = 0; - this._completed = 0; - this._failed = 0; - this._lastOutputMs = 0; - this._atLineStart = true; - this._operations = new Map(); + this.#commandName = undefined; + this.#total = 0; + this.#completed = 0; + this.#failed = 0; + this.#lastOutputMs = 0; + this.#atLineStart = true; + this.#operations = new Map(); } public async initializeAsync(): Promise { @@ -108,8 +108,8 @@ export class PlaintextReporter implements IReporter { public report(event: IReporterEventEnvelope): void { switch (event.type) { case 'commandStarted': { - this._commandName = (event.payload as { commandName: string }).commandName; - this._writeLine(`Starting "rush ${this._commandName}"`); + this.#commandName = (event.payload as { commandName: string }).commandName; + this.#writeLine(`Starting "rush ${this.#commandName}"`); break; } case 'operationRegistered': { @@ -118,20 +118,20 @@ export class PlaintextReporter implements IReporter { projectName?: string; phaseName?: string; }; - this._operations.set(payload.operationId, { + this.#operations.set(payload.operationId, { projectName: payload.projectName ?? payload.operationId, phaseName: payload.phaseName, buffer: [] }); - this._total++; + this.#total++; break; } case 'operationStatusChanged': { - this._onStatusChanged(event); + this.#onStatusChanged(event); break; } case 'externalOutput': { - this._onExternalOutput(event); + this.#onExternalOutput(event); break; } case 'diagnosticEmitted': { @@ -140,17 +140,17 @@ export class PlaintextReporter implements IReporter { severity?: string; }; if (payload.severity === 'error' || payload.severity === 'warning') { - this._writeLine(this._formatDiagnostic(payload.severity, payload.code ?? 'unknown')); + this.#writeLine(this.#formatDiagnostic(payload.severity, payload.code ?? 'unknown')); } break; } case 'watchCycleCompleted': { const succeeded: boolean = (event.payload as { succeeded?: boolean }).succeeded === true; - this._writeLine(`Watch cycle ${succeeded ? 'succeeded' : 'failed'}`); + this.#writeLine(`Watch cycle ${succeeded ? 'succeeded' : 'failed'}`); break; } case 'commandResult': { - this._onResult(event.payload as { commandName: string; succeeded: boolean; exitCode: number }); + this.#onResult(event.payload as { commandName: string; succeeded: boolean; exitCode: number }); break; } default: @@ -171,108 +171,108 @@ export class PlaintextReporter implements IReporter { * last output. Returns whether a heartbeat was emitted. */ public emitHeartbeatIfDue(): boolean { - if (this._nowMs() - this._lastOutputMs >= this._heartbeatIntervalMs) { - this._writeLine( - `... ${this._commandName ?? 'rush'} still running — ${this._completed}/${this._total} operations` + if (this.#nowMs() - this.#lastOutputMs >= this.#heartbeatIntervalMs) { + this.#writeLine( + `... ${this.#commandName ?? 'rush'} still running — ${this.#completed}/${this.#total} operations` ); return true; } return false; } - private _onStatusChanged(event: IReporterEventEnvelope): void { + #onStatusChanged(event: IReporterEventEnvelope): void { const payload: { operationId: string; status: string } = event.payload as { operationId: string; status: string; }; - const record: IOperationRecord | undefined = this._operations.get(payload.operationId); + const record: IOperationRecord | undefined = this.#operations.get(payload.operationId); const projectName: string = record?.projectName ?? event.scope?.projectName ?? payload.operationId; if (!TERMINAL_STATUSES.has(payload.status)) { return; } - this._completed++; + this.#completed++; if (payload.status === 'failure') { - this._failed++; + this.#failed++; } - if (this._variant === 'detailed') { + if (this.#variant === 'detailed') { const phase: string = record?.phaseName ? ` (${record.phaseName})` : ''; - this._writeLine(''); - this._writeLine(`==[ ${projectName}${phase} ]==`); + this.#writeLine(''); + this.#writeLine(`==[ ${projectName}${phase} ]==`); if (record) { - this._writeRaw(record.buffer.join('')); + this.#writeRaw(record.buffer.join('')); record.buffer.length = 0; } - this._writeLine(this._formatStatus(projectName, payload.status)); + this.#writeLine(this.#formatStatus(projectName, payload.status)); } else { - this._writeLine(this._formatStatus(projectName, payload.status)); + this.#writeLine(this.#formatStatus(projectName, payload.status)); } - this._operations.delete(payload.operationId); + this.#operations.delete(payload.operationId); } - private _onExternalOutput(event: IReporterEventEnvelope): void { - if (this._variant !== 'detailed') { + #onExternalOutput(event: IReporterEventEnvelope): void { + if (this.#variant !== 'detailed') { return; } const operationId: string | undefined = event.scope?.operationId; const text: string = (event.payload as { text?: string }).text ?? ''; const record: IOperationRecord | undefined = - operationId !== undefined ? this._operations.get(operationId) : undefined; + operationId !== undefined ? this.#operations.get(operationId) : undefined; if (record) { record.buffer.push(text); } else { - this._writeRaw(text); + this.#writeRaw(text); } } - private _onResult(payload: { commandName: string; succeeded: boolean; exitCode: number }): void { - const commandName: string = payload.commandName ?? this._commandName ?? 'rush'; + #onResult(payload: { commandName: string; succeeded: boolean; exitCode: number }): void { + const commandName: string = payload.commandName ?? this.#commandName ?? 'rush'; if (payload.succeeded) { - this._writeLine( - this._color.green( - `rush ${commandName} succeeded (${this._completed}/${this._total} operations, ${this._failed} failed)` + this.#writeLine( + this.#color.green( + `rush ${commandName} succeeded (${this.#completed}/${this.#total} operations, ${this.#failed} failed)` ) ); } else { - this._writeLine(this._color.red(`rush ${commandName} failed (${this._failed} failed)`)); + this.#writeLine(this.#color.red(`rush ${commandName} failed (${this.#failed} failed)`)); } } - private _formatStatus(projectName: string, status: string): string { + #formatStatus(projectName: string, status: string): string { const line: string = `${projectName}: ${status}`; if (status === 'failure') { - return this._color.red(line); + return this.#color.red(line); } return line; } - private _formatDiagnostic(severity: string, code: string): string { + #formatDiagnostic(severity: string, code: string): string { const line: string = `[${severity}] ${code}`; if (severity === 'error') { - return this._color.red(line); + return this.#color.red(line); } if (severity === 'warning') { - return this._color.yellow(line); + return this.#color.yellow(line); } return line; } - private _writeLine(text: string): void { - if (!this._atLineStart) { - this._write('\n'); + #writeLine(text: string): void { + if (!this.#atLineStart) { + this.#write('\n'); } - this._write(`${text}\n`); - this._atLineStart = true; - this._lastOutputMs = this._nowMs(); + this.#write(`${text}\n`); + this.#atLineStart = true; + this.#lastOutputMs = this.#nowMs(); } - private _writeRaw(text: string): void { - this._write(text); + #writeRaw(text: string): void { + this.#write(text); if (text.length > 0) { - this._atLineStart = text.endsWith('\n'); + this.#atLineStart = text.endsWith('\n'); } - this._lastOutputMs = this._nowMs(); + this.#lastOutputMs = this.#nowMs(); } } diff --git a/libraries/reporter/src/scheduler/OperationStreamEmitter.ts b/libraries/reporter/src/scheduler/OperationStreamEmitter.ts index 259c935a1ea..339800be8f4 100644 --- a/libraries/reporter/src/scheduler/OperationStreamEmitter.ts +++ b/libraries/reporter/src/scheduler/OperationStreamEmitter.ts @@ -58,21 +58,20 @@ export interface IOperationStreamEmitterOptions { * @beta */ export class OperationStreamEmitter { - private readonly _sink: IReporterEventSink; - private readonly _sessionId: string; - private readonly _source: IReporterEventSource; - private readonly _scope: IReporterEventScope | undefined; - private readonly _protocolVersion: IReporterProtocolVersion; - private readonly _maxChunkBytes: number; + readonly #sink: IReporterEventSink; + readonly #sessionId: string; + readonly #source: IReporterEventSource; + readonly #scope: IReporterEventScope | undefined; + readonly #protocolVersion: IReporterProtocolVersion; + readonly #maxChunkBytes: number; public constructor(options: IOperationStreamEmitterOptions) { - this._sink = options.sink; - this._sessionId = options.sessionId; - this._source = options.source; - this._scope = options.scope; - this._protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION; - const maxChunkBytes: number = - options.maxChunkBytes ?? REPORTER_PROTOCOL_LIMITS.externalOutputChunkBytes; + this.#sink = options.sink; + this.#sessionId = options.sessionId; + this.#source = options.source; + this.#scope = options.scope; + this.#protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION; + const maxChunkBytes: number = options.maxChunkBytes ?? REPORTER_PROTOCOL_LIMITS.externalOutputChunkBytes; if ( !Number.isInteger(maxChunkBytes) || maxChunkBytes < 4 || @@ -82,14 +81,14 @@ export class OperationStreamEmitter { `maxChunkBytes must be an integer between 4 and ${REPORTER_PROTOCOL_LIMITS.externalOutputChunkBytes}` ); } - this._maxChunkBytes = maxChunkBytes; + this.#maxChunkBytes = maxChunkBytes; } /** * Emits an operation registration event. */ public registerOperation(operationId: string, projectName?: string, phaseName?: string): string { - return this._emit( + return this.#emit( 'operationRegistered', { operationId, projectName, phaseName }, { operationId, projectName, phaseName }, @@ -101,7 +100,7 @@ export class OperationStreamEmitter { * Emits an operation status transition. */ public changeStatus(operationId: string, status: OperationStatus, durationMs?: number): string { - return this._emit( + return this.#emit( 'operationStatusChanged', { operationId, status, durationMs }, { operationId }, @@ -128,18 +127,18 @@ export class OperationStreamEmitter { const codeUnitCount: number = codePoint > 0xffff ? 2 : 1; const codePointByteLength: number = codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4; - if (end > offset && byteLength + codePointByteLength > this._maxChunkBytes) { + if (end > offset && byteLength + codePointByteLength > this.#maxChunkBytes) { break; } byteLength += codePointByteLength; end += codeUnitCount; - if (byteLength >= this._maxChunkBytes) { + if (byteLength >= this.#maxChunkBytes) { break; } } const chunk: string = text.slice(offset, end); eventIds.push( - this._emit('externalOutput', { stream, text: chunk }, { operationId }, 'local-sensitive') + this.#emit('externalOutput', { stream, text: chunk }, { operationId }, 'local-sensitive') ); offset = end; } @@ -155,7 +154,7 @@ export class OperationStreamEmitter { exitCode: number, operationCounts?: { readonly [status: string]: number } ): string { - return this._emit( + return this.#emit( 'commandResult', { commandName, succeeded, exitCode, operationCounts }, { commandName }, @@ -163,17 +162,17 @@ export class OperationStreamEmitter { ); } - private _emit( + #emit( type: 'operationRegistered' | 'operationStatusChanged' | 'externalOutput' | 'commandResult', payload: unknown, scopeOverride: IReporterEventScope, privacy: 'public' | 'local-sensitive' | 'secret' ): string { - const scope: IReporterEventScope = { ...this._scope, ...scopeOverride }; - return this._sink.emit({ - protocolVersion: this._protocolVersion, - sessionId: this._sessionId, - source: this._source, + const scope: IReporterEventScope = { ...this.#scope, ...scopeOverride }; + return this.#sink.emit({ + protocolVersion: this.#protocolVersion, + sessionId: this.#sessionId, + source: this.#source, scope, privacy, type, diff --git a/libraries/reporter/src/session/RushSessionReporting.ts b/libraries/reporter/src/session/RushSessionReporting.ts index 525d6b49bee..206903e3721 100644 --- a/libraries/reporter/src/session/RushSessionReporting.ts +++ b/libraries/reporter/src/session/RushSessionReporting.ts @@ -69,16 +69,16 @@ export interface IReporterExecutionContext { * @beta */ export class RushSessionReporting { - private readonly _sink: IReporterEventSink; - private readonly _sessionId: string; - private readonly _source: IReporterEventSource; - private readonly _protocolVersion: IReporterProtocolVersion | undefined; + readonly #sink: IReporterEventSink; + readonly #sessionId: string; + readonly #source: IReporterEventSource; + readonly #protocolVersion: IReporterProtocolVersion | undefined; public constructor(options: IRushSessionReportingOptions) { - this._sink = options.sink; - this._sessionId = options.sessionId; - this._source = options.source; - this._protocolVersion = options.protocolVersion; + this.#sink = options.sink; + this.#sessionId = options.sessionId; + this.#source = options.source; + this.#protocolVersion = options.protocolVersion; } /** @@ -86,11 +86,11 @@ export class RushSessionReporting { */ public createScopedReporter(scope?: IReporterEventScope): IScopedReporter { return createScopedReporter({ - sink: this._sink, - sessionId: this._sessionId, - source: this._source, + sink: this.#sink, + sessionId: this.#sessionId, + source: this.#source, scope, - protocolVersion: this._protocolVersion + protocolVersion: this.#protocolVersion }); } @@ -105,7 +105,7 @@ export class RushSessionReporting { * Returns the raw sink handed to actions through the execution context. */ public getSink(): IReporterEventSink { - return this._sink; + return this.#sink; } /** @@ -113,7 +113,7 @@ export class RushSessionReporting { */ public createExecutionContext(scope?: IReporterEventScope): IReporterExecutionContext { return { - sink: this._sink, + sink: this.#sink, reporter: this.createScopedReporter(scope) }; } diff --git a/libraries/reporter/src/telemetry/TelemetrySubscriber.ts b/libraries/reporter/src/telemetry/TelemetrySubscriber.ts index 48b45f50987..9bb7f20ed4a 100644 --- a/libraries/reporter/src/telemetry/TelemetrySubscriber.ts +++ b/libraries/reporter/src/telemetry/TelemetrySubscriber.ts @@ -19,37 +19,37 @@ import type { ITelemetryAggregate, TelemetryResult } from './TelemetryAggregate' * @beta */ export class TelemetrySubscriber { - private _commandName: string | undefined; - private _result: TelemetryResult | undefined; - private _exitCode: number | undefined; - private _durationMs: number | undefined; - private _reporterMode: string | undefined; - private _protocolVersion: IReporterProtocolVersion | undefined; - private readonly _operationStatuses: Map; - private readonly _diagnosticCategoryCounts: { [category: string]: number }; - private readonly _diagnosticCodes: Set; - private readonly _producerVersions: Set; + #commandName: string | undefined; + #result: TelemetryResult | undefined; + #exitCode: number | undefined; + #durationMs: number | undefined; + #reporterMode: string | undefined; + #protocolVersion: IReporterProtocolVersion | undefined; + readonly #operationStatuses: Map; + readonly #diagnosticCategoryCounts: { [category: string]: number }; + readonly #diagnosticCodes: Set; + readonly #producerVersions: Set; public constructor() { - this._operationStatuses = new Map(); - this._diagnosticCategoryCounts = {}; - this._diagnosticCodes = new Set(); - this._producerVersions = new Set(); + this.#operationStatuses = new Map(); + this.#diagnosticCategoryCounts = {}; + this.#diagnosticCodes = new Set(); + this.#producerVersions = new Set(); } /** * Records the selected reporter mode. */ public setReporterMode(reporterMode: string): void { - this._reporterMode = reporterMode; + this.#reporterMode = reporterMode; } /** * Ingests one event, extracting only allowlisted values. */ public ingest(event: IReporterEventEnvelope): void { - this._protocolVersion = event.protocolVersion; - this._producerVersions.add(`${event.source.packageName}@${event.source.packageVersion}`); + this.#protocolVersion = event.protocolVersion; + this.#producerVersions.add(`${event.source.packageName}@${event.source.packageVersion}`); switch (event.type) { case 'commandStarted': { @@ -57,7 +57,7 @@ export class TelemetrySubscriber { break; } // Deliberately ignores argv. - this._commandName = (event.payload as { commandName: string }).commandName; + this.#commandName = (event.payload as { commandName: string }).commandName; break; } case 'commandResult': { @@ -69,9 +69,9 @@ export class TelemetrySubscriber { succeeded: boolean; exitCode: number; }; - this._commandName = payload.commandName; - this._result = payload.succeeded ? 'succeeded' : 'failed'; - this._exitCode = payload.exitCode; + this.#commandName = payload.commandName; + this.#result = payload.succeeded ? 'succeeded' : 'failed'; + this.#exitCode = payload.exitCode; break; } case 'commandCompleted': { @@ -83,11 +83,11 @@ export class TelemetrySubscriber { exitCode: number; durationMs?: number; }; - this._commandName = payload.commandName; - this._exitCode = payload.exitCode; - this._result = payload.exitCode === 0 ? 'succeeded' : 'failed'; + this.#commandName = payload.commandName; + this.#exitCode = payload.exitCode; + this.#result = payload.exitCode === 0 ? 'succeeded' : 'failed'; if (payload.durationMs !== undefined) { - this._durationMs = payload.durationMs; + this.#durationMs = payload.durationMs; } break; } @@ -99,10 +99,10 @@ export class TelemetrySubscriber { exitCode: number; durationMs?: number; }; - this._exitCode = payload.exitCode; - this._result = payload.exitCode === 0 ? 'succeeded' : 'failed'; + this.#exitCode = payload.exitCode; + this.#result = payload.exitCode === 0 ? 'succeeded' : 'failed'; if (payload.durationMs !== undefined) { - this._durationMs = payload.durationMs; + this.#durationMs = payload.durationMs; } break; } @@ -111,7 +111,7 @@ export class TelemetrySubscriber { break; } const payload: IOperationStatusChangedPayload = event.payload as IOperationStatusChangedPayload; - this._operationStatuses.set(payload.operationId, payload.status); + this.#operationStatuses.set(payload.operationId, payload.status); break; } case 'diagnosticEmitted': { @@ -121,11 +121,11 @@ export class TelemetrySubscriber { category?: string; }; if (payload.code !== undefined) { - this._diagnosticCodes.add(payload.code); + this.#diagnosticCodes.add(payload.code); } if (payload.category !== undefined) { - this._diagnosticCategoryCounts[payload.category] = - (this._diagnosticCategoryCounts[payload.category] ?? 0) + 1; + this.#diagnosticCategoryCounts[payload.category] = + (this.#diagnosticCategoryCounts[payload.category] ?? 0) + 1; } break; } @@ -142,7 +142,7 @@ export class TelemetrySubscriber { */ public buildAggregate(): ITelemetryAggregate { const operationStatusCounts: { [status: string]: number } = {}; - for (const status of this._operationStatuses.values()) { + for (const status of this.#operationStatuses.values()) { operationStatusCounts[status] = (operationStatusCounts[status] ?? 0) + 1; } @@ -159,28 +159,28 @@ export class TelemetrySubscriber { producerVersions: string[]; } = { operationStatusCounts, - diagnosticCodes: [...this._diagnosticCodes].sort(), - diagnosticCategoryCounts: { ...this._diagnosticCategoryCounts }, - producerVersions: [...this._producerVersions].sort() + diagnosticCodes: [...this.#diagnosticCodes].sort(), + diagnosticCategoryCounts: { ...this.#diagnosticCategoryCounts }, + producerVersions: [...this.#producerVersions].sort() }; - if (this._commandName !== undefined) { - aggregate.commandName = this._commandName; + if (this.#commandName !== undefined) { + aggregate.commandName = this.#commandName; } - if (this._result !== undefined) { - aggregate.result = this._result; + if (this.#result !== undefined) { + aggregate.result = this.#result; } - if (this._exitCode !== undefined) { - aggregate.exitCode = this._exitCode; + if (this.#exitCode !== undefined) { + aggregate.exitCode = this.#exitCode; } - if (this._durationMs !== undefined) { - aggregate.durationMs = this._durationMs; + if (this.#durationMs !== undefined) { + aggregate.durationMs = this.#durationMs; } - if (this._reporterMode !== undefined) { - aggregate.reporterMode = this._reporterMode; + if (this.#reporterMode !== undefined) { + aggregate.reporterMode = this.#reporterMode; } - if (this._protocolVersion !== undefined) { - aggregate.protocolVersion = this._protocolVersion; + if (this.#protocolVersion !== undefined) { + aggregate.protocolVersion = this.#protocolVersion; } return aggregate; diff --git a/libraries/rush-lib/src/cli/RushCommandLineParser.ts b/libraries/rush-lib/src/cli/RushCommandLineParser.ts index 2b0e40aec07..e5d5378f0ed 100644 --- a/libraries/rush-lib/src/cli/RushCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushCommandLineParser.ts @@ -84,6 +84,20 @@ export interface IRushCommandLineParserOptions { reporterCloseAsync?: () => Promise; } +type ReportErrorAndSetExitCodeTestOverride = typeof _setReportErrorAndSetExitCodeForTesting & { + callback?: (error: Error) => never; +}; + +/** + * Overrides error reporting for unit tests that need thrown errors instead of process termination. + * @internal + */ +export function _setReportErrorAndSetExitCodeForTesting( + callback: ((error: Error) => never) | undefined +): void { + (_setReportErrorAndSetExitCodeForTesting as ReportErrorAndSetExitCodeTestOverride).callback = callback; +} + export class RushCommandLineParser extends CommandLineParser { public telemetry: Telemetry | undefined; public rushGlobalFolder: RushGlobalFolder; @@ -167,7 +181,7 @@ export class RushCommandLineParser extends CommandLineParser { this.rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFilePath); } } catch (error) { - this._reportInitializationErrorAndSetExitCode(error as Error); + this.#reportInitializationErrorAndSetExitCode(error as Error); } NodeJsCompatibility.warnAboutCompatibilityIssues({ @@ -210,7 +224,7 @@ export class RushCommandLineParser extends CommandLineParser { try { this.#addCommandLineConfigActions(commandLineConfiguration); } catch (e) { - this._reportInitializationErrorAndSetExitCode( + this.#reportInitializationErrorAndSetExitCode( new Error( `Error from plugin ${pluginLoader.pluginName} by ${pluginLoader.packageName}: ${( e as Error @@ -266,7 +280,7 @@ export class RushCommandLineParser extends CommandLineParser { public override async executeAsync(args?: string[]): Promise { if (this.#initializationFailed) { - await this._closeReporterAsync(); + await this.#closeReporterAsync(); return false; } @@ -279,7 +293,7 @@ export class RushCommandLineParser extends CommandLineParser { this.#terminalProvider.verboseEnabled = this.#terminalProvider.debugEnabled = rushArgv.includes('--debug') || rushArgv.includes('-d'); - this._startReporterSession(); + this.#startReporterSession(); try { await measureAsyncFn('rush:initializeUnassociatedPlugins', () => @@ -288,17 +302,17 @@ export class RushCommandLineParser extends CommandLineParser { const succeeded: boolean = await super.executeAsync(args); if (!this.#reporterCompletionEmitted) { - this._emitReporterCompletion(succeeded ? 0 : _getNumericProcessExitCode(1)); + this.#emitReporterCompletion(succeeded ? 0 : _getNumericProcessExitCode(1)); } return succeeded; } catch (error) { if (!process.exitCode) { process.exitCode = 1; } - this._reportErrorAndSetExitCode(error as Error); + this.#reportErrorAndSetExitCode(error as Error); return false; } finally { - await this._closeReporterAsync(); + await this.#closeReporterAsync(); } } @@ -307,7 +321,7 @@ export class RushCommandLineParser extends CommandLineParser { await super.executeWithoutErrorHandlingAsync(args); } catch (error) { // Capture the original parse error before the base executeAsync renders it and returns false. - this._emitReporterFailureDiagnostic(error as Error, !this.#commandLifecycleEmitter); + this.#emitReporterFailureDiagnostic(error as Error, !this.#commandLifecycleEmitter); throw error; } } @@ -376,14 +390,14 @@ export class RushCommandLineParser extends CommandLineParser { // If we make it here, everything went fine, so reset the exit code back to 0 process.exitCode = 0; } catch (error) { - this._reportErrorAndSetExitCode(error as Error); + this.#reportErrorAndSetExitCode(error as Error); } // This only gets hit if the wrapped execution completes successfully try { await this.telemetry?.ensureFlushedAsync(); } catch (error) { - this._emitReporterFailureDiagnostic(error as Error); + this.#emitReporterFailureDiagnostic(error as Error); throw error; } } @@ -444,7 +458,7 @@ export class RushCommandLineParser extends CommandLineParser { this.#populateScriptActions(); } catch (error) { - this._reportInitializationErrorAndSetExitCode(error as Error); + this.#reportInitializationErrorAndSetExitCode(error as Error); } } @@ -593,7 +607,7 @@ export class RushCommandLineParser extends CommandLineParser { ); } - private _startReporterSession(): void { + #startReporterSession(): void { if (this.#sessionLifecycleEmitter && this.#sessionStartTimeMs === undefined) { this.#sessionStartTimeMs = performance.now(); this.#sessionLifecycleEmitter.emitSessionStarted({ @@ -602,8 +616,8 @@ export class RushCommandLineParser extends CommandLineParser { } } - private _emitReporterFailureDiagnostic(error: Error, includeMessage: boolean = false): void { - this._startReporterSession(); + #emitReporterFailureDiagnostic(error: Error, includeMessage: boolean = false): void { + this.#startReporterSession(); const emitter: LifecycleEmitter | undefined = this.#commandLifecycleEmitter ?? this.#sessionLifecycleEmitter; const rushSession: RushSession | undefined = this.rushSession; @@ -629,8 +643,15 @@ export class RushCommandLineParser extends CommandLineParser { } } - private _reportErrorAndSetExitCode(error: Error): void { - this._emitReporterFailureDiagnostic(error); + #reportErrorAndSetExitCode(error: Error): void { + const testOverride: ((error: Error) => never) | undefined = ( + _setReportErrorAndSetExitCodeForTesting as ReportErrorAndSetExitCodeTestOverride + ).callback; + if (testOverride) { + testOverride(error); + } + + this.#emitReporterFailureDiagnostic(error); if (!(error instanceof AlreadyReportedError)) { const prefix: string = 'ERROR: '; @@ -659,7 +680,7 @@ export class RushCommandLineParser extends CommandLineParser { ? numericExitCode : 1; process.exitCode = exitCode; - this._emitReporterCompletion(exitCode); + this.#emitReporterCompletion(exitCode); this.flushTelemetry(); const handleExit = (): never => { @@ -680,7 +701,7 @@ export class RushCommandLineParser extends CommandLineParser { if (this.#rushOptions.reporterCloseAsync || telemetryFlushAsync) { const pendingFlushes: Promise[] = []; if (this.#rushOptions.reporterCloseAsync) { - pendingFlushes.push(this._closeReporterAsync()); + pendingFlushes.push(this.#closeReporterAsync()); } if (telemetryFlushAsync) { pendingFlushes.push(telemetryFlushAsync); @@ -691,12 +712,12 @@ export class RushCommandLineParser extends CommandLineParser { } } - private _reportInitializationErrorAndSetExitCode(error: Error): void { + #reportInitializationErrorAndSetExitCode(error: Error): void { this.#initializationFailed = true; - this._reportErrorAndSetExitCode(error); + this.#reportErrorAndSetExitCode(error); } - private _closeReporterAsync(): Promise { + #closeReporterAsync(): Promise { if (!this.#reporterClosePromise) { this.#reporterClosePromise = (async (): Promise => { try { @@ -710,7 +731,7 @@ export class RushCommandLineParser extends CommandLineParser { return this.#reporterClosePromise; } - private _emitReporterCompletion(exitCode: number): void { + #emitReporterCompletion(exitCode: number): void { if (!this.#sessionLifecycleEmitter || this.#reporterCompletionEmitted) { return; } diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index 30dda7645a7..1d78a32f2ed 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -68,6 +68,18 @@ export interface IRushPnpmCommandLineParserOptions { terminalProvider?: ITerminalProvider; } +/** + * Controlled dependencies for exercising post-execution behavior in unit tests. + * @internal + */ +export interface IRushPnpmCommandLineParserTestOptions { + commandName: string; + doRushUpdateAsync: () => Promise; + rushConfiguration: RushConfiguration; + subspace: Subspace; + terminal: ITerminal; +} + function _reportErrorAndSetExitCode(error: Error, terminal: ITerminal, debugEnabled: boolean): never { if (!(error instanceof AlreadyReportedError)) { const prefix: string = 'ERROR: '; @@ -84,21 +96,31 @@ function _reportErrorAndSetExitCode(error: Error, terminal: ITerminal, debugEnab } export class RushPnpmCommandLineParser { - private readonly _terminal: ITerminal; - private readonly _rushConfiguration: RushConfiguration; - private readonly _pnpmArgs: string[]; - private _commandName: string | undefined; - private readonly _debugEnabled: boolean; - private _subspace: Subspace; + readonly #terminal: ITerminal; + readonly #rushConfiguration: RushConfiguration; + readonly #pnpmArgs: string[]; + #commandName: string | undefined; + readonly #debugEnabled: boolean; + #subspace: Subspace; + readonly #doRushUpdateAsyncOverride: (() => Promise) | undefined; private constructor( options: IRushPnpmCommandLineParserOptions, terminal: ITerminal, - debugEnabled: boolean + debugEnabled: boolean, + testOptions?: IRushPnpmCommandLineParserTestOptions ) { - this._debugEnabled = debugEnabled; - - this._terminal = terminal; + this.#debugEnabled = debugEnabled; + this.#terminal = terminal; + this.#doRushUpdateAsyncOverride = testOptions?.doRushUpdateAsync; + + if (testOptions) { + this.#rushConfiguration = testOptions.rushConfiguration; + this.#pnpmArgs = []; + this.#commandName = testOptions.commandName; + this.#subspace = testOptions.subspace; + return; + } // Are we in a Rush repo? const rushJsonFilePath: string | undefined = RushConfiguration.tryFindRushJsonLocation({ @@ -123,7 +145,7 @@ export class RushPnpmCommandLineParser { 'The "rush-pnpm" command must be executed in a folder that is under a Rush workspace folder' ); } - this._rushConfiguration = rushConfiguration; + this.#rushConfiguration = rushConfiguration; if (rushConfiguration.packageManager !== 'pnpm') { throw new Error( @@ -162,24 +184,24 @@ export class RushPnpmCommandLineParser { pnpmArgs = process.argv.slice(2); } - this._pnpmArgs = pnpmArgs; + this.#pnpmArgs = pnpmArgs; const subspace: Subspace = rushConfiguration.getSubspace(subspaceName); - this._subspace = subspace; + this.#subspace = subspace; const workspaceFolder: string = subspace.getSubspaceTempFolderPath(); const workspaceFilePath: string = `${workspaceFolder}/${RushConstants.pnpmWorkspaceFileName}`; if (!FileSystem.exists(workspaceFilePath)) { - this._terminal.writeErrorLine('Error: The PNPM workspace file has not been generated:'); - this._terminal.writeErrorLine(` ${workspaceFilePath}\n`); - this._terminal.writeLine(Colorize.cyan(`Do you need to run "rush install" or "rush update"?`)); + this.#terminal.writeErrorLine('Error: The PNPM workspace file has not been generated:'); + this.#terminal.writeErrorLine(` ${workspaceFilePath}\n`); + this.#terminal.writeLine(Colorize.cyan(`Do you need to run "rush install" or "rush update"?`)); throw new AlreadyReportedError(); } if (!FileSystem.exists(rushConfiguration.packageManagerToolFilename)) { - this._terminal.writeErrorLine('Error: The PNPM local binary has not been installed yet.'); - this._terminal.writeLine('\n' + Colorize.cyan(`Do you need to run "rush install" or "rush update"?`)); + this.#terminal.writeErrorLine('Error: The PNPM local binary has not been installed yet.'); + this.#terminal.writeLine('\n' + Colorize.cyan(`Do you need to run "rush install" or "rush update"?`)); throw new AlreadyReportedError(); } } @@ -203,26 +225,60 @@ export class RushPnpmCommandLineParser { terminal, debugEnabled ); - await rushPnpmCommandLineParser._validatePnpmUsageAsync(rushPnpmCommandLineParser._pnpmArgs); + await rushPnpmCommandLineParser.#validatePnpmUsageAsync(rushPnpmCommandLineParser.#pnpmArgs); return rushPnpmCommandLineParser; } catch (error) { _reportErrorAndSetExitCode(error as Error, terminal, debugEnabled); } } + /** + * Exercises argument validation without invoking the side-effectful constructor. + * @internal + */ + public static async _validatePnpmUsageForTestingAsync(pnpmArgs: string[]): Promise { + const terminal: Terminal = new Terminal(new ConsoleTerminalProvider()); + const rushConfiguration: RushConfiguration = Object.create(RushConfiguration.prototype); + const subspace: Subspace = Object.create(null); + const parser: RushPnpmCommandLineParser = new RushPnpmCommandLineParser({}, terminal, false, { + commandName: '', + doRushUpdateAsync: async () => {}, + rushConfiguration, + subspace, + terminal + }); + await parser.#validatePnpmUsageAsync(pnpmArgs); + } + + /** + * Exercises post-execution synchronization with controlled dependencies. + * @internal + */ + public static async _postExecuteForTestingAsync( + testOptions: IRushPnpmCommandLineParserTestOptions + ): Promise { + const parser: RushPnpmCommandLineParser = new RushPnpmCommandLineParser( + {}, + testOptions.terminal, + false, + testOptions + ); + await parser.#postExecuteAsync(); + } + public async executeAsync(): Promise { // Node.js can sometimes accidentally terminate with a zero exit code (e.g. for an uncaught // promise exception), so we start with the assumption that the exit code is 1 // and set it to 0 only on success. process.exitCode = 1; - await this._executeAsync(); + await this.#executeAsync(); if (process.exitCode === 0) { - await this._postExecuteAsync(); + await this.#postExecuteAsync(); } } - private async _validatePnpmUsageAsync(pnpmArgs: string[]): Promise { + async #validatePnpmUsageAsync(pnpmArgs: string[]): Promise { if (pnpmArgs[0] === RUSH_SKIP_CHECKS_PARAMETER) { pnpmArgs.shift(); // Ignore other checks @@ -249,10 +305,10 @@ export class RushPnpmCommandLineParser { if (!/^[a-z]+([a-z0-9\-])*$/.test(firstArg)) { // We can't parse this CLI syntax - this._terminal.writeErrorLine( + this.#terminal.writeErrorLine( `Warning: The "rush-pnpm" wrapper expects a command verb before "${firstArg}"\n` ); - this._terminal.writeLine(Colorize.cyan(BYPASS_NOTICE)); + this.#terminal.writeLine(Colorize.cyan(BYPASS_NOTICE)); throw new AlreadyReportedError(); } else { const commandName: string = firstArg; @@ -266,7 +322,7 @@ export class RushPnpmCommandLineParser { if (pnpmArgs.indexOf(RUSH_SKIP_CHECKS_PARAMETER) >= 0) { // We do not attempt to parse PNPM's complete CLI syntax, so we cannot be sure how to interpret // strings that appear outside of the specific patterns that this parser recognizes - this._terminal.writeErrorLine( + this.#terminal.writeErrorLine( PrintUtilities.wrapWords( `Error: The "${RUSH_SKIP_CHECKS_PARAMETER}" option must be the first parameter for the "rush-pnpm" command.` ) @@ -274,7 +330,7 @@ export class RushPnpmCommandLineParser { throw new AlreadyReportedError(); } - this._commandName = commandName; + this.#commandName = commandName; _addDefaultRecursiveFlagIfNeeded(commandName, pnpmArgs); // Warn about commands known not to work @@ -282,12 +338,12 @@ export class RushPnpmCommandLineParser { switch (commandName) { // Blocked case 'import': { - this._terminal.writeErrorLine( + this.#terminal.writeErrorLine( PrintUtilities.wrapWords( `Error: The "pnpm ${commandName}" command is known to be incompatible with Rush's environment.` ) + '\n' ); - this._terminal.writeLine(Colorize.cyan(BYPASS_NOTICE)); + this.#terminal.writeLine(Colorize.cyan(BYPASS_NOTICE)); throw new AlreadyReportedError(); } @@ -299,13 +355,13 @@ export class RushPnpmCommandLineParser { case 'install-test': /* synonym */ case 'it': { - this._terminal.writeErrorLine( + this.#terminal.writeErrorLine( PrintUtilities.wrapWords( `Error: The "pnpm ${commandName}" command is incompatible with Rush's environment.` + ` Use the "rush install" or "rush update" commands instead.` ) + '\n' ); - this._terminal.writeLine(Colorize.cyan(BYPASS_NOTICE)); + this.#terminal.writeLine(Colorize.cyan(BYPASS_NOTICE)); throw new AlreadyReportedError(); } @@ -320,12 +376,12 @@ export class RushPnpmCommandLineParser { case 'update': /* synonym */ case 'up': { - this._terminal.writeWarningLine( + this.#terminal.writeWarningLine( PrintUtilities.wrapWords( `Warning: The "pnpm ${commandName}" command makes changes that may invalidate Rush's workspace state.` ) + '\n' ); - this._terminal.writeWarningLine( + this.#terminal.writeWarningLine( `==> Consider running "rush install" or "rush update" afterwards.\n` ); break; @@ -339,8 +395,8 @@ export class RushPnpmCommandLineParser { * For instance, /usr/bin/patch which may just hangs forever * So, erroring out the command if the pnpm version is < 7.4.0 */ - if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '7.4.0')) { - this._terminal.writeErrorLine( + if (semver.lt(this.#rushConfiguration.packageManagerToolVersion, '7.4.0')) { + this.#terminal.writeErrorLine( PrintUtilities.wrapWords( `Error: The "pnpm patch" command is added after pnpm@7.4.0.` + ` Please update "pnpmVersion" >= 7.4.0 in ${RushConstants.rushJsonFilename} file and run "rush update" to use this command.` @@ -352,11 +408,11 @@ export class RushPnpmCommandLineParser { } case 'patch-commit': { const pnpmOptionsJsonFilename: string = path.join( - this._rushConfiguration.commonRushConfigFolder, + this.#rushConfiguration.commonRushConfigFolder, RushConstants.pnpmConfigFilename ); - if (this._rushConfiguration.rushConfigurationJson.pnpmOptions) { - this._terminal.writeErrorLine( + if (this.#rushConfiguration.rushConfigurationJson.pnpmOptions) { + this.#terminal.writeErrorLine( PrintUtilities.wrapWords( `Error: The "pnpm patch-commit" command is incompatible with specifying "pnpmOptions" in ${RushConstants.rushJsonFilename} file.` + ` Please move the content of "pnpmOptions" in ${RushConstants.rushJsonFilename} file to ${pnpmOptionsJsonFilename}` @@ -371,8 +427,8 @@ export class RushPnpmCommandLineParser { /** * The "patch-remove" command was introduced in pnpm version 8.5.0 */ - if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '8.5.0')) { - this._terminal.writeErrorLine( + if (semver.lt(this.#rushConfiguration.packageManagerToolVersion, '8.5.0')) { + this.#terminal.writeErrorLine( PrintUtilities.wrapWords( `Error: The "pnpm patch-remove" command is added after pnpm@8.5.0.` + ` Please update "pnpmVersion" >= 8.5.0 in ${RushConstants.rushJsonFilename} file and run "rush update" to use this command.` @@ -389,8 +445,8 @@ export class RushPnpmCommandLineParser { * to approve packages for running build scripts when onlyBuiltDependencies is used. * In PNPM 11.0.0, it was updated to use allowBuilds in pnpm-workspace.yaml. */ - if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '10.1.0')) { - this._terminal.writeErrorLine( + if (semver.lt(this.#rushConfiguration.packageManagerToolVersion, '10.1.0')) { + this.#terminal.writeErrorLine( PrintUtilities.wrapWords( `Error: The "pnpm approve-builds" command is added after pnpm@10.1.0.` + ` Please update "pnpmVersion" >= 10.1.0 in ${RushConstants.rushJsonFilename} file and run "rush update" to use this command.` @@ -399,11 +455,11 @@ export class RushPnpmCommandLineParser { throw new AlreadyReportedError(); } const pnpmOptionsJsonFilename: string = path.join( - this._rushConfiguration.commonRushConfigFolder, + this.#rushConfiguration.commonRushConfigFolder, RushConstants.pnpmConfigFilename ); - if (this._rushConfiguration.rushConfigurationJson.pnpmOptions) { - this._terminal.writeErrorLine( + if (this.#rushConfiguration.rushConfigurationJson.pnpmOptions) { + this.#terminal.writeErrorLine( PrintUtilities.wrapWords( `Error: The "pnpm approve-builds" command is incompatible with specifying "pnpmOptions" in ${RushConstants.rushJsonFilename} file.` + ` Please move the content of "pnpmOptions" in ${RushConstants.rushJsonFilename} file to ${pnpmOptionsJsonFilename}` @@ -440,21 +496,21 @@ export class RushPnpmCommandLineParser { // Unknown default: { - this._terminal.writeErrorLine( + this.#terminal.writeErrorLine( PrintUtilities.wrapWords( `Error: The "pnpm ${commandName}" command has not been tested with Rush's environment. It may be incompatible.` ) + '\n' ); - this._terminal.writeLine(Colorize.cyan(BYPASS_NOTICE)); + this.#terminal.writeLine(Colorize.cyan(BYPASS_NOTICE)); } } /* eslint-enable no-fallthrough */ } } - private async _executeAsync(): Promise { - const rushConfiguration: RushConfiguration = this._rushConfiguration; - const workspaceFolder: string = this._subspace.getSubspaceTempFolderPath(); + async #executeAsync(): Promise { + const rushConfiguration: RushConfiguration = this.#rushConfiguration; + const workspaceFolder: string = this.#subspace.getSubspaceTempFolderPath(); const pnpmEnvironmentMap: EnvironmentMap = new EnvironmentMap(process.env); pnpmEnvironmentMap.set('NPM_CONFIG_WORKSPACE_DIR', workspaceFolder); @@ -491,14 +547,14 @@ export class RushPnpmCommandLineParser { } let onStdoutStreamChunk: ((chunk: string) => string | void) | undefined; - switch (this._commandName) { + switch (this.#commandName) { case 'patch': { // Replace `pnpm patch-commit` with `rush-pnpm patch-commit` when running // `pnpm patch` to avoid the `pnpm patch` command being suggested in the output onStdoutStreamChunk = (stdoutChunk: string) => { return stdoutChunk.replace( /pnpm patch-commit/g, - `rush-pnpm --subspace ${this._subspace.subspaceName} patch-commit` + `rush-pnpm --subspace ${this.#subspace.subspaceName} patch-commit` ); }; @@ -509,7 +565,7 @@ export class RushPnpmCommandLineParser { try { const { exitCode } = await Utilities.executeCommandAsync({ command: rushConfiguration.packageManagerToolFilename, - args: this._pnpmArgs, + args: this.#pnpmArgs, workingDirectory: process.cwd(), environment: pnpmEnvironmentMap.toObject(), keepEnvironment: true, @@ -524,17 +580,17 @@ export class RushPnpmCommandLineParser { process.exitCode = 1; } } catch (e) { - this._terminal.writeDebugLine(`Error: ${e}`); + this.#terminal.writeDebugLine(`Error: ${e}`); } } - private async _postExecuteAsync(): Promise { - const commandName: string | undefined = this._commandName; + async #postExecuteAsync(): Promise { + const commandName: string | undefined = this.#commandName; if (!commandName) { return; } - const subspaceTempFolder: string = this._subspace.getSubspaceTempFolderPath(); + const subspaceTempFolder: string = this.#subspace.getSubspaceTempFolderPath(); switch (commandName) { case 'patch-remove': @@ -543,17 +599,17 @@ export class RushPnpmCommandLineParser { // 1. pnpm-config.json is required for `rush-pnpm patch-commit`. Rush writes the patched dependency to the pnpm-config.json when finishes. // 2. we can not fallback to use Monorepo config folder (common/config/rush) due to that this command is intended to apply to input subspace only. // It will produce unexpected behavior if we use the fallback. - if (this._subspace.getPnpmOptions() === undefined) { - const subspaceConfigFolder: string = this._subspace.getSubspaceConfigFolderPath(); - this._terminal.writeErrorLine( + if (this.#subspace.getPnpmOptions() === undefined) { + const subspaceConfigFolder: string = this.#subspace.getSubspaceConfigFolderPath(); + this.#terminal.writeErrorLine( `The "rush-pnpm patch-commit" command cannot proceed without a pnpm-config.json file.` + ` Create one in this folder: ${subspaceConfigFolder}` ); break; } - const pnpmOptions: PnpmOptionsConfiguration | undefined = this._subspace.getPnpmOptions(); - const pnpmVersion: string = this._rushConfiguration.packageManagerToolVersion; + const pnpmOptions: PnpmOptionsConfiguration | undefined = this.#subspace.getPnpmOptions(); + const pnpmVersion: string = this.#rushConfiguration.packageManagerToolVersion; const semver: typeof import('semver') = await import('semver'); let newGlobalPatchedDependencies: Record | undefined; @@ -576,7 +632,7 @@ export class RushPnpmCommandLineParser { if (!Objects.areDeepEqual(currentGlobalPatchedDependencies, newGlobalPatchedDependencies)) { const commonTempPnpmPatchesFolder: string = `${subspaceTempFolder}/${RushConstants.pnpmPatchesFolderName}`; - const rushPnpmPatchesFolder: string = this._subspace.getSubspacePnpmPatchesFolderPath(); + const rushPnpmPatchesFolder: string = this.#subspace.getSubspacePnpmPatchesFolderPath(); // Copy (or delete) common\temp\subspace\patches\ --> common\config\pnpm-patches\ OR common\config\rush\pnpm-patches\ if (FileSystem.exists(commonTempPnpmPatchesFolder)) { @@ -601,9 +657,9 @@ export class RushPnpmCommandLineParser { pnpmOptions?.updateGlobalPatchedDependencies(newGlobalPatchedDependencies); // Rerun installation to update - await this._doRushUpdateAsync(); + await this.#doRushUpdateAsync(); - this._terminal.writeWarningLine( + this.#terminal.writeWarningLine( `Rush refreshed the ${RushConstants.pnpmConfigFilename}, shrinkwrap file and patch files under the ` + `"${commonTempPnpmPatchesFolder}" folder.\n` + ' Please commit this change to Git.' @@ -612,17 +668,17 @@ export class RushPnpmCommandLineParser { break; } case 'approve-builds': { - if (this._subspace.getPnpmOptions() === undefined) { - const subspaceConfigFolder: string = this._subspace.getSubspaceConfigFolderPath(); - this._terminal.writeErrorLine( + if (this.#subspace.getPnpmOptions() === undefined) { + const subspaceConfigFolder: string = this.#subspace.getSubspaceConfigFolderPath(); + this.#terminal.writeErrorLine( `The "rush-pnpm approve-builds" command cannot proceed without a pnpm-config.json file.` + ` Create one in this folder: ${subspaceConfigFolder}` ); break; } - const pnpmOptions: PnpmOptionsConfiguration | undefined = this._subspace.getPnpmOptions(); - const pnpmVersion: string = this._rushConfiguration.packageManagerToolVersion; + const pnpmOptions: PnpmOptionsConfiguration | undefined = this.#subspace.getPnpmOptions(); + const pnpmVersion: string = this.#rushConfiguration.packageManagerToolVersion; const semver: typeof import('semver') = await import('semver'); if (semver.gte(pnpmVersion, '11.0.0')) { @@ -639,9 +695,9 @@ export class RushPnpmCommandLineParser { pnpmOptions?.updateGlobalAllowBuilds(newGlobalAllowBuilds); // Rerun installation to update - await this._doRushUpdateAsync(); + await this.#doRushUpdateAsync(); - this._terminal.writeWarningLine( + this.#terminal.writeWarningLine( `Rush refreshed the ${RushConstants.pnpmConfigFilename} and shrinkwrap file.\n` + ' Please commit this change to Git.' ); @@ -661,9 +717,9 @@ export class RushPnpmCommandLineParser { await pnpmOptions?.updateGlobalOnlyBuiltDependenciesAsync(newGlobalOnlyBuiltDependencies); // Rerun installation to update - await this._doRushUpdateAsync(); + await this.#doRushUpdateAsync(); - this._terminal.writeWarningLine( + this.#terminal.writeWarningLine( `Rush refreshed the ${RushConstants.pnpmConfigFilename} and shrinkwrap file.\n` + ' Please commit this change to Git.' ); @@ -677,7 +733,7 @@ export class RushPnpmCommandLineParser { // generated "catalogs" section of common/temp//pnpm-workspace.yaml. That file is // regenerated on every install, so the updated versions must be synced back to the // "globalCatalogs" field of pnpm-config.json for the change to be persisted. - const pnpmOptions: PnpmOptionsConfiguration | undefined = this._subspace.getPnpmOptions(); + const pnpmOptions: PnpmOptionsConfiguration | undefined = this.#subspace.getPnpmOptions(); if (pnpmOptions === undefined) { break; } @@ -694,9 +750,9 @@ export class RushPnpmCommandLineParser { if (!Objects.areDeepEqual(currentGlobalCatalogs, newGlobalCatalogs)) { await pnpmOptions.updateGlobalCatalogsAsync(newGlobalCatalogs); - await this._doRushUpdateAsync(); + await this.#doRushUpdateAsync(); - this._terminal.writeWarningLine( + this.#terminal.writeWarningLine( `Rush refreshed the ${RushConstants.pnpmConfigFilename} and shrinkwrap file.\n` + ' Please commit this change to Git.' ); @@ -706,15 +762,20 @@ export class RushPnpmCommandLineParser { } } - private async _doRushUpdateAsync(): Promise { - this._terminal.writeLine(); - this._terminal.writeLine(Colorize.green('Running "rush update"')); - this._terminal.writeLine(); + async #doRushUpdateAsync(): Promise { + if (this.#doRushUpdateAsyncOverride) { + await this.#doRushUpdateAsyncOverride(); + return; + } + + this.#terminal.writeLine(); + this.#terminal.writeLine(Colorize.green('Running "rush update"')); + this.#terminal.writeLine(); const rushGlobalFolder: RushGlobalFolder = new RushGlobalFolder(); - const purgeManager: PurgeManager = new PurgeManager(this._rushConfiguration, rushGlobalFolder); + const purgeManager: PurgeManager = new PurgeManager(this.#rushConfiguration, rushGlobalFolder); const installManagerOptions: IInstallManagerOptions = { - debug: this._debugEnabled, + debug: this.#debugEnabled, allowShrinkwrapUpdates: true, bypassPolicy: false, noLink: false, @@ -726,10 +787,10 @@ export class RushPnpmCommandLineParser { variant: process.env[EnvironmentVariableNames.RUSH_VARIANT], // For `rush-pnpm`, only use the env var maxInstallAttempts: RushConstants.defaultMaxInstallAttempts, pnpmFilterArgumentValues: [], - selectedProjects: new Set(this._rushConfiguration.projects), + selectedProjects: new Set(this.#rushConfiguration.projects), checkOnly: false, - subspace: this._subspace, - terminal: this._terminal + subspace: this.#subspace, + terminal: this.#terminal }; const installManagerFactoryModule: typeof import('../logic/InstallManagerFactory') = await import( @@ -738,7 +799,7 @@ export class RushPnpmCommandLineParser { ); const installManager: BaseInstallManager = await installManagerFactoryModule.InstallManagerFactory.getInstallManagerAsync( - this._rushConfiguration, + this.#rushConfiguration, rushGlobalFolder, purgeManager, installManagerOptions diff --git a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts index d067e8bb5ce..fd3e8add8fe 100644 --- a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts +++ b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts @@ -4,37 +4,33 @@ import * as path from 'node:path'; import { FileSystem, JsonFile } from '@rushstack/node-core-library'; +import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; import { RushConfiguration } from '../../api/RushConfiguration'; import type { Subspace } from '../../api/Subspace'; import { RushPnpmCommandLineParser } from '../RushPnpmCommandLineParser'; -interface IRushPnpmCommandLineParserInternals { - _validatePnpmUsageAsync(pnpmArgs: string[]): Promise; -} - async function validatePnpmArgsAsync(pnpmArgs: string[]): Promise { - const parser: IRushPnpmCommandLineParserInternals = Object.create(RushPnpmCommandLineParser.prototype); - await parser._validatePnpmUsageAsync(pnpmArgs); + await RushPnpmCommandLineParser._validatePnpmUsageForTestingAsync(pnpmArgs); return pnpmArgs; } const SUBSPACE_TEMP_FOLDER: string = '/repo/common/temp'; -function createPostExecuteParser(options: { +function createPostExecuteOptions(options: { commandName: string; pnpmVersion: string; globalPatchedDependencies: Record | undefined; updateGlobalPatchedDependencies: jest.Mock; doRushUpdateAsync: jest.Mock; -}): RushPnpmCommandLineParser { - const parser: RushPnpmCommandLineParser = Object.create(RushPnpmCommandLineParser.prototype); - Object.assign(parser, { - _commandName: options.commandName, - _rushConfiguration: { packageManagerToolVersion: options.pnpmVersion }, - _terminal: { writeWarningLine: jest.fn(), writeErrorLine: jest.fn() }, - _doRushUpdateAsync: options.doRushUpdateAsync, - _subspace: { +}): Parameters[0] { + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider()); + return { + commandName: options.commandName, + rushConfiguration: { packageManagerToolVersion: options.pnpmVersion } as RushConfiguration, + terminal, + doRushUpdateAsync: options.doRushUpdateAsync, + subspace: { getSubspaceTempFolderPath: () => SUBSPACE_TEMP_FOLDER, getSubspaceConfigFolderPath: () => '/repo/common/config/rush', getSubspacePnpmPatchesFolderPath: () => '/repo/common/config/rush/pnpm-patches', @@ -42,9 +38,8 @@ function createPostExecuteParser(options: { globalPatchedDependencies: options.globalPatchedDependencies, updateGlobalPatchedDependencies: options.updateGlobalPatchedDependencies }) - } - }); - return parser; + } as unknown as Subspace + }; } describe(RushPnpmCommandLineParser.name, () => { @@ -74,34 +69,27 @@ describe(`${RushPnpmCommandLineParser.name} catalog sync`, () => { const TEST_TEMP_FOLDER: string = `${PACKAGE_ROOT}/temp/rush-pnpm-catalog-sync-test`; const FIXTURE_FOLDER: string = `${__dirname}/catalogSyncTestRepo`; - interface IRushPnpmCommandLineParserCatalogInternals { - _commandName: string; - _subspace: Subspace; - _terminal: { writeWarningLine(message: string): void }; - _doRushUpdateAsync(): Promise; - _postExecuteAsync(): Promise; - } - function createParserForCommand( repoFolder: string, commandName: string - ): { parser: IRushPnpmCommandLineParserCatalogInternals; pnpmConfigFilename: string } { + ): { + parserOptions: Parameters[0]; + pnpmConfigFilename: string; + } { const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( `${repoFolder}/rush.json` ); const subspace: Subspace = rushConfiguration.defaultSubspace; - - const parser: IRushPnpmCommandLineParserCatalogInternals = Object.create( - RushPnpmCommandLineParser.prototype - ); - parser._commandName = commandName; - parser._subspace = subspace; - parser._terminal = { writeWarningLine: () => {} }; - // Avoid triggering a real "rush update" - parser._doRushUpdateAsync = async () => {}; + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider()); return { - parser, + parserOptions: { + commandName, + doRushUpdateAsync: async () => {}, + rushConfiguration, + subspace, + terminal + }, pnpmConfigFilename: `${repoFolder}/common/config/rush/pnpm-config.json` }; } @@ -132,8 +120,8 @@ describe(`${RushPnpmCommandLineParser.name} catalog sync`, () => { ].join('\n'); await FileSystem.writeFileAsync(workspaceYamlFilename, bumpedWorkspaceYaml); - const { parser, pnpmConfigFilename } = createParserForCommand(TEST_TEMP_FOLDER, 'up'); - await parser._postExecuteAsync(); + const { parserOptions, pnpmConfigFilename } = createParserForCommand(TEST_TEMP_FOLDER, 'up'); + await RushPnpmCommandLineParser._postExecuteForTestingAsync(parserOptions); const updatedConfig: { globalCatalogs?: Record> } = await JsonFile.loadAsync(pnpmConfigFilename); @@ -146,14 +134,13 @@ describe(`${RushPnpmCommandLineParser.name} catalog sync`, () => { }); it('does not modify pnpm-config.json when the catalog is unchanged', async () => { - const { parser, pnpmConfigFilename } = createParserForCommand(TEST_TEMP_FOLDER, 'up'); + const { parserOptions, pnpmConfigFilename } = createParserForCommand(TEST_TEMP_FOLDER, 'up'); const originalContent: string = await FileSystem.readFileAsync(pnpmConfigFilename); - const doRushUpdateSpy: jest.SpyInstance = jest - .spyOn(parser, '_doRushUpdateAsync') - .mockResolvedValue(undefined); + const doRushUpdateSpy: jest.Mock = jest.fn(); + parserOptions.doRushUpdateAsync = doRushUpdateSpy; - await parser._postExecuteAsync(); + await RushPnpmCommandLineParser._postExecuteForTestingAsync(parserOptions); // The fixture's pnpm-workspace.yaml already matches pnpm-config.json, so nothing should change expect(await FileSystem.readFileAsync(pnpmConfigFilename)).toEqual(originalContent); @@ -165,7 +152,7 @@ describe(`${RushPnpmCommandLineParser.name} patch-commit patchedDependencies syn it('reads patchedDependencies from pnpm-workspace.yaml for pnpm >= 11', async () => { const updateGlobalPatchedDependencies: jest.Mock = jest.fn(); const doRushUpdateAsync: jest.Mock = jest.fn(); - const parser: RushPnpmCommandLineParser = createPostExecuteParser({ + const parserOptions = createPostExecuteOptions({ commandName: 'patch-commit', pnpmVersion: '11.7.0', globalPatchedDependencies: { 'left-pad@1.0.0': 'patches/left-pad@1.0.0.patch' }, @@ -186,7 +173,7 @@ describe(`${RushPnpmCommandLineParser.name} patch-commit patchedDependencies syn .spyOn(JsonFile, 'load') .mockReturnValue({ pnpm: { patchedDependencies: { 'should-not-be-used@1.0.0': 'x.patch' } } }); - await parser['_postExecuteAsync'](); + await RushPnpmCommandLineParser._postExecuteForTestingAsync(parserOptions); expect(readFileAsyncSpy).toHaveBeenCalledWith(`${SUBSPACE_TEMP_FOLDER}/pnpm-workspace.yaml`); expect(jsonLoadSpy).not.toHaveBeenCalled(); @@ -199,7 +186,7 @@ describe(`${RushPnpmCommandLineParser.name} patch-commit patchedDependencies syn it('reads patchedDependencies from package.json for pnpm < 11', async () => { const updateGlobalPatchedDependencies: jest.Mock = jest.fn(); const doRushUpdateAsync: jest.Mock = jest.fn(); - const parser: RushPnpmCommandLineParser = createPostExecuteParser({ + const parserOptions = createPostExecuteOptions({ commandName: 'patch-commit', pnpmVersion: '10.27.0', globalPatchedDependencies: { 'left-pad@1.0.0': 'patches/left-pad@1.0.0.patch' }, @@ -212,7 +199,7 @@ describe(`${RushPnpmCommandLineParser.name} patch-commit patchedDependencies syn pnpm: { patchedDependencies: { 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' } } }); - await parser['_postExecuteAsync'](); + await RushPnpmCommandLineParser._postExecuteForTestingAsync(parserOptions); expect(jsonLoadSpy).toHaveBeenCalledWith(`${SUBSPACE_TEMP_FOLDER}/package.json`); expect(readFileAsyncSpy).not.toHaveBeenCalled(); diff --git a/libraries/rush-lib/src/cli/test/mockRushCommandLineParser.ts b/libraries/rush-lib/src/cli/test/mockRushCommandLineParser.ts index 9904cfcabf8..5c7666ad347 100644 --- a/libraries/rush-lib/src/cli/test/mockRushCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/test/mockRushCommandLineParser.ts @@ -15,9 +15,8 @@ function mockReportErrorAndSetExitCode(error: Error): void { jest.mock('../RushCommandLineParser', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const actualModule: any = jest.requireActual('../RushCommandLineParser'); - if (actualModule.RushCommandLineParser) { - // Stub out the troublesome method that calls `process.exit` - actualModule.RushCommandLineParser.prototype._reportErrorAndSetExitCode = mockReportErrorAndSetExitCode; + if (actualModule._setReportErrorAndSetExitCodeForTesting) { + actualModule._setReportErrorAndSetExitCodeForTesting(mockReportErrorAndSetExitCode); } return actualModule; }); diff --git a/libraries/rush-lib/src/logic/Telemetry.ts b/libraries/rush-lib/src/logic/Telemetry.ts index 71c5d74f64a..8c6ac3fab72 100644 --- a/libraries/rush-lib/src/logic/Telemetry.ts +++ b/libraries/rush-lib/src/logic/Telemetry.ts @@ -161,7 +161,7 @@ export class Telemetry { #dataFolder: string; #rushConfiguration: RushConfiguration; #rushSession: RushSession; - private readonly _flushAsyncTasks: Set> = new Set(); + readonly #flushAsyncTasks: Set> = new Set(); #telemetryStartTime: number = 0; public constructor(rushConfiguration: RushConfiguration, rushSession: RushSession) { @@ -234,13 +234,13 @@ export class Telemetry { * and store the promise into a list so that we can await it later. */ const asyncTaskPromise: Promise = this.#rushSession.hooks.flushTelemetry.promise(this.#store); - this._flushAsyncTasks.add(asyncTaskPromise); + this.#flushAsyncTasks.add(asyncTaskPromise); asyncTaskPromise.then( () => { - this._flushAsyncTasks.delete(asyncTaskPromise); + this.#flushAsyncTasks.delete(asyncTaskPromise); }, () => { - this._flushAsyncTasks.delete(asyncTaskPromise); + this.#flushAsyncTasks.delete(asyncTaskPromise); } ); } @@ -253,7 +253,7 @@ export class Telemetry { * There are some async tasks that are not finished when the process is exiting. */ public async ensureFlushedAsync(): Promise { - await Promise.all(this._flushAsyncTasks); + await Promise.all(this.#flushAsyncTasks); } public get store(): ITelemetryData[] { diff --git a/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts b/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts index bfcd964a6f0..317c146471e 100644 --- a/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts +++ b/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts @@ -76,6 +76,24 @@ interface IPathsToCache { outputFilePaths: string[]; } +let _tryCollectPathsToCacheForTestingAsync: + | ((subject: OperationBuildCache, terminal: ITerminal) => Promise) + | undefined; + +/** + * Exercises output path collection through a unit-test-only module export. + * @internal + */ +export async function _tryCollectPathsToCacheAsyncForTesting( + subject: OperationBuildCache, + terminal: ITerminal +): Promise { + if (!_tryCollectPathsToCacheForTestingAsync) { + throw new InternalError('OperationBuildCache test accessor was not initialized.'); + } + return await _tryCollectPathsToCacheForTestingAsync(subject, terminal); +} + function _getDirectFileTransferLockResourceName(cacheId: string): string { // LockFile resource names must match /^[a-zA-Z0-9][a-zA-Z0-9-.]+[a-zA-Z0-9]$/, but cacheId may // contain other characters (e.g. "/") depending on the configured cacheEntryNamePattern, so hash @@ -129,6 +147,13 @@ export function _setTarUtilityPromiseForTesting( * @internal */ export class OperationBuildCache { + static { + _tryCollectPathsToCacheForTestingAsync = async ( + subject: OperationBuildCache, + terminal: ITerminal + ): Promise => await subject.#tryCollectPathsToCacheAsync(terminal); + } + readonly #project: RushConfigurationProject; readonly #localBuildCacheProvider: FileSystemBuildCacheProvider; readonly #cloudBuildCacheProvider: ICloudBuildCacheProvider | undefined; @@ -380,7 +405,7 @@ export class OperationBuildCache { return false; } - const filesToCache: IPathsToCache | undefined = await this._tryCollectPathsToCacheAsync(terminal); + const filesToCache: IPathsToCache | undefined = await this.#tryCollectPathsToCacheAsync(terminal); if (!filesToCache) { return false; } @@ -495,7 +520,7 @@ export class OperationBuildCache { * @returns The list of output files as project-relative paths, or `undefined` if a * symbolic link was encountered. */ - private async _tryCollectPathsToCacheAsync(terminal: ITerminal): Promise { + async #tryCollectPathsToCacheAsync(terminal: ITerminal): Promise { const projectFolderPath: string = this.#project.projectFolder; const outputFilePaths: string[] = []; const queue: [string, string][] = []; diff --git a/libraries/rush-lib/src/logic/buildCache/test/OperationBuildCache.test.ts b/libraries/rush-lib/src/logic/buildCache/test/OperationBuildCache.test.ts index 3431d5eb460..1a944aa8172 100644 --- a/libraries/rush-lib/src/logic/buildCache/test/OperationBuildCache.test.ts +++ b/libraries/rush-lib/src/logic/buildCache/test/OperationBuildCache.test.ts @@ -10,7 +10,11 @@ import type { IGenerateCacheEntryIdOptions } from '../CacheEntryId'; import type { FileSystemBuildCacheProvider } from '../FileSystemBuildCacheProvider'; import type { TarExecutable } from '../../../utilities/TarExecutable'; -import { OperationBuildCache, _setTarUtilityPromiseForTesting } from '../OperationBuildCache'; +import { + OperationBuildCache, + _setTarUtilityPromiseForTesting, + _tryCollectPathsToCacheAsyncForTesting +} from '../OperationBuildCache'; interface ITestOptions { enabled: boolean; @@ -71,9 +75,7 @@ describe(OperationBuildCache.name, () => { describe(OperationBuildCache.getOperationBuildCache.name, () => { it('returns an OperationBuildCache with a calculated cacheId value', () => { const subject: OperationBuildCache = prepareSubject({}); - expect(subject.cacheId).toMatchInlineSnapshot( - `"acme-wizard/1926f30e8ed24cb47be89aea39e7efd70fcda075"` - ); + expect(subject.cacheId).toMatchInlineSnapshot(`"acme-wizard/1926f30e8ed24cb47be89aea39e7efd70fcda075"`); }); }); @@ -277,7 +279,7 @@ describe(OperationBuildCache.name, () => { const terminal: Terminal = new Terminal(terminalProvider); const result: { outputFilePaths: string[]; filteredOutputFolderNames: string[] } | undefined = - await subject['_tryCollectPathsToCacheAsync'](terminal); + await _tryCollectPathsToCacheAsyncForTesting(subject, terminal); expect(result).toBeDefined(); expect(result!.outputFilePaths).toEqual(['dist/bar.js', 'dist/foo.txt']); @@ -298,7 +300,7 @@ describe(OperationBuildCache.name, () => { const terminal: Terminal = new Terminal(terminalProvider); const result: { outputFilePaths: string[]; filteredOutputFolderNames: string[] } | undefined = - await subject['_tryCollectPathsToCacheAsync'](terminal); + await _tryCollectPathsToCacheAsyncForTesting(subject, terminal); expect(result).toBeDefined(); expect(result!.outputFilePaths).toEqual(['dist/._orphan.txt', 'dist/other.js']); @@ -317,7 +319,7 @@ describe(OperationBuildCache.name, () => { const terminal: Terminal = new Terminal(terminalProvider); const result: { outputFilePaths: string[]; filteredOutputFolderNames: string[] } | undefined = - await subject['_tryCollectPathsToCacheAsync'](terminal); + await _tryCollectPathsToCacheAsyncForTesting(subject, terminal); expect(result).toBeDefined(); expect(result!.outputFilePaths).toEqual(['dist/._foo.txt', 'dist/foo.txt']); @@ -336,7 +338,7 @@ describe(OperationBuildCache.name, () => { const terminal: Terminal = new Terminal(terminalProvider); const result: { outputFilePaths: string[]; filteredOutputFolderNames: string[] } | undefined = - await subject['_tryCollectPathsToCacheAsync'](terminal); + await _tryCollectPathsToCacheAsyncForTesting(subject, terminal); expect(result).toBeDefined(); expect(result!.outputFilePaths).toEqual(['dist/._foo.txt', 'dist/foo.txt']); @@ -355,7 +357,7 @@ describe(OperationBuildCache.name, () => { const terminal: Terminal = new Terminal(terminalProvider); const result: { outputFilePaths: string[]; filteredOutputFolderNames: string[] } | undefined = - await subject['_tryCollectPathsToCacheAsync'](terminal); + await _tryCollectPathsToCacheAsyncForTesting(subject, terminal); expect(result).toBeDefined(); expect(result!.outputFilePaths).toEqual(['dist/._', 'dist/other.txt']); @@ -384,7 +386,7 @@ describe(OperationBuildCache.name, () => { const terminal: Terminal = new Terminal(terminalProvider); const result: { outputFilePaths: string[]; filteredOutputFolderNames: string[] } | undefined = - await subject['_tryCollectPathsToCacheAsync'](terminal); + await _tryCollectPathsToCacheAsyncForTesting(subject, terminal); expect(result).toBeDefined(); expect(result!.outputFilePaths).toEqual(['dist/index.js', 'dist/sub/nested.js']); diff --git a/libraries/rush-lib/src/logic/operations/ReporterOperationEventSink.ts b/libraries/rush-lib/src/logic/operations/ReporterOperationEventSink.ts index d3287c44270..f9b85fd1fc0 100644 --- a/libraries/rush-lib/src/logic/operations/ReporterOperationEventSink.ts +++ b/libraries/rush-lib/src/logic/operations/ReporterOperationEventSink.ts @@ -38,13 +38,12 @@ interface IReporterOperationCycle { } class ReporterOperationEventSink implements IOperationGraphEventSink { - private readonly _operationsByLegacyId: Map = new Map(); - private readonly _cyclesByResult: WeakMap = - new WeakMap(); - private readonly _rushSession: RushSession; + readonly #operationsByLegacyId: Map = new Map(); + readonly #cyclesByResult: WeakMap = new WeakMap(); + readonly #rushSession: RushSession; public constructor(rushSession: RushSession, commandName: string, operations: Iterable) { - this._rushSession = rushSession; + this.#rushSession = rushSession; const operationsByReporterId: Map = new Map(); for (const operation of operations) { @@ -73,12 +72,12 @@ class ReporterOperationEventSink implements IOperationGraphEventSink { operationsByReporterId.set(operationId, reporterOperation); } reporterOperation.legacyOperationIds.add(operation.name); - this._operationsByLegacyId.set(operation.name, reporterOperation); + this.#operationsByLegacyId.set(operation.name, reporterOperation); } } public get isEnabled(): boolean { - return this._operationsByLegacyId.size > 0; + return this.#operationsByLegacyId.size > 0; } public onOperationRegistered( @@ -86,7 +85,7 @@ class ReporterOperationEventSink implements IOperationGraphEventSink { silent: boolean, result?: IOperationExecutionResult ): void { - const operation: IReporterOperation | undefined = this._operationsByLegacyId.get(operationId); + const operation: IReporterOperation | undefined = this.#operationsByLegacyId.get(operationId); if (!operation || !result) { return; } @@ -103,7 +102,7 @@ class ReporterOperationEventSink implements IOperationGraphEventSink { operation.registrationCycle = cycle; } - this._cyclesByResult.set(result, cycle); + this.#cyclesByResult.set(result, cycle); cycle.registeredOperationIds.add(operationId); cycle.silent &&= silent; if (cycle.registeredOperationIds.size !== operation.legacyOperationIds.size || cycle.silent) { @@ -118,11 +117,11 @@ class ReporterOperationEventSink implements IOperationGraphEventSink { } public onOperationStatusChanged(result: IOperationExecutionResult): void { - const operation: IReporterOperation | undefined = this._operationsByLegacyId.get(result.operation.name); + const operation: IReporterOperation | undefined = this.#operationsByLegacyId.get(result.operation.name); if (!operation) { return; } - const cycle: IReporterOperationCycle | undefined = this._cyclesByResult.get(result); + const cycle: IReporterOperationCycle | undefined = this.#cyclesByResult.get(result); if (!cycle) { return; } @@ -144,7 +143,7 @@ class ReporterOperationEventSink implements IOperationGraphEventSink { }); operation.emitter.emitDiagnostic(diagnostic); if (result.error) { - _correlateRushSessionError(this._rushSession, result.error, diagnostic.diagnosticId); + _correlateRushSessionError(this.#rushSession, result.error, diagnostic.diagnosticId); } } @@ -171,12 +170,12 @@ class CompositeOperationGraphEventSink implements IOperationGraphEventSink { public readonly onOperationChunk: ((operationId: string, chunk: ITerminalChunk) => void) | undefined; public readonly onOperationStreamClosed: ((operationId: string) => void) | undefined; - private readonly _first: IOperationGraphEventSink; - private readonly _second: IOperationGraphEventSink; + readonly #first: IOperationGraphEventSink; + readonly #second: IOperationGraphEventSink; public constructor(first: IOperationGraphEventSink, second: IOperationGraphEventSink) { - this._first = first; - this._second = second; + this.#first = first; + this.#second = second; this.onOperationChunk = first.onOperationChunk || second.onOperationChunk ? (operationId, chunk) => { @@ -198,23 +197,23 @@ class CompositeOperationGraphEventSink implements IOperationGraphEventSink { silent: boolean, result?: IOperationExecutionResult ): void { - this._first.onOperationRegistered?.(operationId, silent, result); - this._second.onOperationRegistered?.(operationId, silent, result); + this.#first.onOperationRegistered?.(operationId, silent, result); + this.#second.onOperationRegistered?.(operationId, silent, result); } public onOperationStatusChanged(result: IOperationExecutionResult, previousStatus: OperationStatus): void { - this._first.onOperationStatusChanged?.(result, previousStatus); - this._second.onOperationStatusChanged?.(result, previousStatus); + this.#first.onOperationStatusChanged?.(result, previousStatus); + this.#second.onOperationStatusChanged?.(result, previousStatus); } public onOperationHeader(operationId: string, completedOperations: number, totalOperations: number): void { - this._first.onOperationHeader?.(operationId, completedOperations, totalOperations); - this._second.onOperationHeader?.(operationId, completedOperations, totalOperations); + this.#first.onOperationHeader?.(operationId, completedOperations, totalOperations); + this.#second.onOperationHeader?.(operationId, completedOperations, totalOperations); } public onActivity(text: string, options?: IOperationActivityOptions): void { - this._first.onActivity?.(text, options); - this._second.onActivity?.(text, options); + this.#first.onActivity?.(text, options); + this.#second.onActivity?.(text, options); } } diff --git a/libraries/rush-lib/src/logic/test/Telemetry.test.ts b/libraries/rush-lib/src/logic/test/Telemetry.test.ts index 6bff8eab19e..6228383ec82 100644 --- a/libraries/rush-lib/src/logic/test/Telemetry.test.ts +++ b/libraries/rush-lib/src/logic/test/Telemetry.test.ts @@ -19,10 +19,6 @@ class CapturingSink implements IReporterEventSink { } } -interface ITelemetryPrivateMembers extends Omit { - _flushAsyncTasks: Set>; -} - describe(Telemetry.name, () => { const mockedJsonFileSave: jest.SpyInstance = jest.spyOn(JsonFile, 'save').mockImplementation(() => { /* don't actually write anything */ @@ -187,10 +183,7 @@ describe(Telemetry.name, () => { }); const customFlushTelemetry: jest.Mock = jest.fn(); rushSession.hooks.flushTelemetry.tap('test', customFlushTelemetry); - const telemetry: ITelemetryPrivateMembers = new Telemetry( - rushConfig, - rushSession - ) as unknown as ITelemetryPrivateMembers; + const telemetry: Telemetry = new Telemetry(rushConfig, rushSession); const logData: ITelemetryData = { name: 'testData1', durationInSeconds: 100, @@ -203,9 +196,6 @@ describe(Telemetry.name, () => { expect(customFlushTelemetry.mock.calls[0][0][0]).toEqual(expect.objectContaining(logData)); await telemetry.ensureFlushedAsync(); - - // Ensure the tasks get cleaned up - expect(telemetry._flushAsyncTasks.size).toEqual(0); }); it('calls custom flush telemetry twice', async () => { @@ -217,10 +207,7 @@ describe(Telemetry.name, () => { }); const customFlushTelemetry: jest.Mock = jest.fn(); rushSession.hooks.flushTelemetry.tap('test', customFlushTelemetry); - const telemetry: ITelemetryPrivateMembers = new Telemetry( - rushConfig, - rushSession - ) as unknown as ITelemetryPrivateMembers; + const telemetry: Telemetry = new Telemetry(rushConfig, rushSession); const logData: ITelemetryData = { name: 'testData1', durationInSeconds: 100, @@ -244,8 +231,5 @@ describe(Telemetry.name, () => { expect(customFlushTelemetry.mock.calls[1][0][0]).toEqual(expect.objectContaining(logData2)); await telemetry.ensureFlushedAsync(); - - // Ensure the tasks get cleaned up - expect(telemetry._flushAsyncTasks.size).toEqual(0); }); }); diff --git a/libraries/rush-lib/src/pluginFramework/PluginLoader/PluginLoaderBase.ts b/libraries/rush-lib/src/pluginFramework/PluginLoader/PluginLoaderBase.ts index 013cce44e26..d9fd335af06 100644 --- a/libraries/rush-lib/src/pluginFramework/PluginLoader/PluginLoaderBase.ts +++ b/libraries/rush-lib/src/pluginFramework/PluginLoader/PluginLoaderBase.ts @@ -53,7 +53,7 @@ export abstract class PluginLoaderBase< protected readonly _terminal: ITerminal; protected _manifestCache: Readonly | undefined; - private _packageVersionCache: string | undefined; + #packageVersionCache: string | undefined; /** * The folder that should be used for resolving the plugin's NPM package. @@ -88,17 +88,17 @@ export abstract class PluginLoaderBase< } public get packageVersion(): string { - if (!this._packageVersionCache) { + if (!this.#packageVersionCache) { const packageJson: IPackageJson = PackageJsonLookup.instance.loadPackageJson( path.join(this.packageFolder, 'package.json') ); if (!packageJson.version) { throw new InternalError(`Rush plugin package "${this.packageName}" does not specify a version.`); } - this._packageVersionCache = packageJson.version; + this.#packageVersionCache = packageJson.version; } - return this._packageVersionCache; + return this.#packageVersionCache; } public getCommandLineConfiguration(): CommandLineConfiguration | undefined { diff --git a/libraries/rush-terminal-renderer/src/OperationHeaderTracker.ts b/libraries/rush-terminal-renderer/src/OperationHeaderTracker.ts index 1ee286ec649..106edecf2b0 100644 --- a/libraries/rush-terminal-renderer/src/OperationHeaderTracker.ts +++ b/libraries/rush-terminal-renderer/src/OperationHeaderTracker.ts @@ -7,32 +7,31 @@ const INITIAL_OPERATION_COUNT: number = 0; const OPERATION_COUNT_INCREMENT: number = 1; export class OperationHeaderTracker { - private readonly _headerByOperation: Map = new Map(); - private _completedOperations: number = INITIAL_OPERATION_COUNT; - private _totalOperations: number = INITIAL_OPERATION_COUNT; + readonly #headerByOperation: Map = new Map(); + #completedOperations: number = INITIAL_OPERATION_COUNT; + #totalOperations: number = INITIAL_OPERATION_COUNT; public registerOperation(): void { - this._totalOperations += OPERATION_COUNT_INCREMENT; + this.#totalOperations += OPERATION_COUNT_INCREMENT; } public setOperationHeader(header: IDaemonOperationHeaderPayload): void { - this._headerByOperation.set(header.operationId, header); + this.#headerByOperation.set(header.operationId, header); } public takeOperationHeader(operationId: string): IDaemonOperationHeaderPayload { - const header: IDaemonOperationHeaderPayload | undefined = - this._headerByOperation.get(operationId); + const header: IDaemonOperationHeaderPayload | undefined = this.#headerByOperation.get(operationId); if (header !== undefined) { - this._headerByOperation.delete(operationId); - this._completedOperations = header.completedOperations; - this._totalOperations = header.totalOperations; + this.#headerByOperation.delete(operationId); + this.#completedOperations = header.completedOperations; + this.#totalOperations = header.totalOperations; return header; } - this._completedOperations += OPERATION_COUNT_INCREMENT; + this.#completedOperations += OPERATION_COUNT_INCREMENT; return { - completedOperations: this._completedOperations, + completedOperations: this.#completedOperations, operationId, - totalOperations: this._totalOperations + totalOperations: this.#totalOperations }; } } diff --git a/rush-plugins/rush-amazon-s3-build-cache-plugin/src/AmazonS3Client.ts b/rush-plugins/rush-amazon-s3-build-cache-plugin/src/AmazonS3Client.ts index 76fee1c85b6..aa0bc39ea9b 100644 --- a/rush-plugins/rush-amazon-s3-build-cache-plugin/src/AmazonS3Client.ts +++ b/rush-plugins/rush-amazon-s3-build-cache-plugin/src/AmazonS3Client.ts @@ -252,7 +252,7 @@ export class AmazonS3Client { } } - private _writeWarningLine(...messageParts: string[]): void { + #writeWarningLine(...messageParts: string[]): void { // if the terminal has been closed then don't bother sending a warning message try { this.#terminal.writeWarningLine(...messageParts); @@ -287,7 +287,7 @@ export class AmazonS3Client { cleanup?.(); // unauthorized due to not providing credentials, // silence error for better DX when e.g. running locally without credentials - this._writeWarningLine( + this.#writeWarningLine( `No credentials found and received a ${status}`, ' response code from the cloud storage.', ' Maybe run rush update-cloud-credentials', diff --git a/rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/AmazonS3Client.test.ts b/rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/AmazonS3Client.test.ts index a4f2e104e51..7f5a212f1b2 100644 --- a/rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/AmazonS3Client.test.ts +++ b/rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/AmazonS3Client.test.ts @@ -469,15 +469,11 @@ describe(AmazonS3Client.name, () => { for (const code of [400, 401, 403]) { it(`Handles missing credentials object when ${code}`, async () => { - let warningSpy: jest.SpyInstance | undefined; + const warningSpy = jest.spyOn(terminal, 'writeWarningLine').mockImplementation(() => {}); const result: Buffer | undefined = await makeS3ClientRequestAsync( undefined, DUMMY_OPTIONS, async (s3Client) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (s3Client as any)._writeWarningLine = () => {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - warningSpy = jest.spyOn(s3Client as any, '_writeWarningLine'); return await s3Client.getObjectAsync('abc123'); }, { diff --git a/rush-plugins/rush-buildxl-graph-plugin/src/test/GraphProcessor.test.ts b/rush-plugins/rush-buildxl-graph-plugin/src/test/GraphProcessor.test.ts index 905d0383af9..caa1f4a0491 100644 --- a/rush-plugins/rush-buildxl-graph-plugin/src/test/GraphProcessor.test.ts +++ b/rush-plugins/rush-buildxl-graph-plugin/src/test/GraphProcessor.test.ts @@ -22,7 +22,7 @@ class MockRunner implements IOperationRunner { declare public cacheable: boolean; declare public reportTiming: boolean; declare public warningsAreAllowed: boolean; - declare private _configHash: string; + declare public _configHash: string; public async executeAsync(): Promise { throw new Error('Method not implemented.'); diff --git a/rush-plugins/rush-mcp-docs-plugin/src/DocsTool.ts b/rush-plugins/rush-mcp-docs-plugin/src/DocsTool.ts index f0f830b339f..6601868a0f2 100644 --- a/rush-plugins/rush-mcp-docs-plugin/src/DocsTool.ts +++ b/rush-plugins/rush-mcp-docs-plugin/src/DocsTool.ts @@ -38,7 +38,7 @@ export class DocsTool implements IRushMcpTool { } // TODO: replace with Microsoft's service - private _searchDocs(query: string): IDocsResult { + #searchDocs(query: string): IDocsResult { const startTime: number = Date.now(); const results: IDocsResult['results'] = JsonFile.load( @@ -54,7 +54,7 @@ export class DocsTool implements IRushMcpTool { } public async executeAsync({ userQuery }: zodModule.infer): Promise { - const docSearchResult: IDocsResult = this._searchDocs(userQuery); + const docSearchResult: IDocsResult = this.#searchDocs(userQuery); return { content: [