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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions apps/rush/src/IRushFrontendLaunchOptions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { ILaunchOptions } from '@microsoft/rush-lib';
import type { IReporterEventSink } from '@rushstack/rush-reporter';
import type { ILaunchOptions, IRushSessionReporterOptions } from '@microsoft/rush-lib';

/**
* The cross-version launch contract owned by the Rush frontend.
Expand All @@ -13,6 +12,6 @@ import type { IReporterEventSink } from '@rushstack/rush-reporter';
* options, so an older engine can safely ignore the new property.
*/
export interface IRushFrontendLaunchOptions extends ILaunchOptions {
readonly reporterEventSink: IReporterEventSink;
readonly reporter: IRushSessionReporterOptions;
readonly reporterCloseAsync: () => Promise<void>;
}
10 changes: 9 additions & 1 deletion apps/rush/src/RushFrontend.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { randomUUID } from 'node:crypto';

import type { ILaunchOptions } from '@microsoft/rush-lib';
import { DEFAULT_SIGNAL_FLUSH_TIMEOUT_MS } from '@rushstack/rush-reporter';

Expand Down Expand Up @@ -30,6 +32,7 @@ export interface IRushFrontendOptions {
currentRushLib: typeof import('@microsoft/rush-lib'),
launchOptions: IRushFrontendLaunchOptions
) => void | Promise<void>;
readonly createSessionId?: () => string;
readonly processLifecycle?: IRushFrontendProcessLifecycle;
}

Expand Down Expand Up @@ -132,6 +135,7 @@ export async function launchRushFrontendAsync(options: IRushFrontendOptions): Pr
initializeReporterHostAsync = initializeRushReporterHostAsync,
createVersionSelector = (version: string) => new RushVersionSelector(version),
executeCurrentRush = RushCommandSelector.execute,
createSessionId = randomUUID,
processLifecycle = createProcessLifecycle()
} = options;

Expand All @@ -152,9 +156,13 @@ export async function launchRushFrontendAsync(options: IRushFrontendOptions): Pr
}
const reporterCloseAsync: () => Promise<void> = () =>
reporterLifecycle?.closeAsync() ?? reporterHost.closeAsync();
const sessionId: string = createSessionId();
const reporterLaunchOptions: IRushFrontendLaunchOptions = {
...launchOptions,
reporterEventSink: reporterHost.sink,
reporter: {
eventSink: reporterHost.sink,
sessionId
},
reporterCloseAsync
};

Expand Down
57 changes: 50 additions & 7 deletions apps/rush/src/test/RushFrontend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as os from 'node:os';
import * as path from 'node:path';

import * as rushLib from '@microsoft/rush-lib';
import type { ILaunchOptions } from '@microsoft/rush-lib';
import { EnvironmentConfiguration } from '@microsoft/rush-lib/lib/api/EnvironmentConfiguration';
import { RushCommandLineParser } from '@microsoft/rush-lib/lib/cli/RushCommandLineParser';
import {
Expand All @@ -18,6 +19,7 @@ import {
} from '@rushstack/rush-reporter';

import { launchRushFrontendAsync, type IRushFrontendProcessLifecycle } from '../RushFrontend';
import type { IRushFrontendLaunchOptions } from '../IRushFrontendLaunchOptions';
import {
initializeRushReporterHostAsync,
type IInitializedRushReporterHost,
Expand Down Expand Up @@ -178,7 +180,7 @@ function emitCommandStarted(sink: IReporterEventSink): void {
describe(launchRushFrontendAsync.name, () => {
it('creates the authoritative host before invoking the bundled rush-lib and passes only its sink', async () => {
const order: string[] = [];
let receivedOptions: Record<string, unknown> | undefined;
let receivedOptions: IRushFrontendLaunchOptions | undefined;
const processLifecycle: ITestProcessLifecycle = createTestProcessLifecycle();
const originalArgv: string[] = process.argv;
process.argv = ['node', 'rush', 'build', '--reporter=legacy', '--json'];
Expand All @@ -195,17 +197,18 @@ describe(launchRushFrontendAsync.name, () => {
void version;
void selectedRushLib;
order.push('engine');
receivedOptions = launchOptions as unknown as Record<string, unknown>;
receivedOptions = launchOptions;
return launchOptions.reporterCloseAsync();
},
processLifecycle
});

expect(order).toEqual(['host', 'engine', 'close']);
expect(process.argv).toEqual(['node', 'rush', 'build', '--json']);
expect(receivedOptions?.reporterEventSink).toEqual(
expect.objectContaining({ emit: expect.any(Function) }) as IReporterEventSink
);
expect(receivedOptions?.reporter).toEqual({
eventSink: expect.objectContaining({ emit: expect.any(Function) }),
sessionId: expect.any(String)
});
expect(receivedOptions).not.toHaveProperty('selection');
expect(receivedOptions).not.toHaveProperty('host');
expect(receivedOptions).not.toHaveProperty('manager');
Expand All @@ -216,6 +219,46 @@ describe(launchRushFrontendAsync.name, () => {
}
});

it('passes one typed reporter session through the real Rush launch boundary', async () => {
const order: string[] = [];
const initialized: IInitializedRushReporterHost = await createInitializedHostAsync(order);
const createSessionId: jest.Mock<string, []> = jest.fn(() => 'session-from-frontend');
let receivedOptions: ILaunchOptions | undefined;
const launchSpy: jest.SpyInstance = jest
.spyOn(rushLib.Rush, 'launch')
.mockImplementation((version, launchOptions) => {
void version;
receivedOptions = launchOptions;
});
const originalArgv: string[] = process.argv;
process.argv = ['node', 'rush', 'build'];

try {
await launchRushFrontendAsync({
currentPackageVersion: '5.178.1',
rushVersionToLoad: undefined,
configuration: undefined,
launchOptions: { isManaged: false },
currentRushLib: rushLib,
initializeReporterHostAsync: async () => initialized,
createSessionId,
processLifecycle: createTestProcessLifecycle()
});

expect(launchSpy).toHaveBeenCalledTimes(1);
expect(createSessionId).toHaveBeenCalledTimes(1);
expect(receivedOptions?.reporter).toEqual({
eventSink: initialized.sink,
sessionId: 'session-from-frontend'
});
await initialized.closeAsync();
expect(order).toEqual(['host', 'close']);
} finally {
launchSpy.mockRestore();
process.argv = originalArgv;
}
});

it('rejects an explicit reporter before initializing an incompatible selected engine', async () => {
const directory: string = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rush-old-engine-'));
const outputPath: string = path.join(directory, 'events.jsonl');
Expand Down Expand Up @@ -603,7 +646,7 @@ describe(launchRushFrontendAsync.name, () => {
executeCurrentRush: (version, selectedRushLib, launchOptions) => {
void version;
void selectedRushLib;
emitCommandStarted(launchOptions.reporterEventSink);
emitCommandStarted(launchOptions.reporter.eventSink);
return launchOptions.reporterCloseAsync();
},
processLifecycle: createTestProcessLifecycle()
Expand Down Expand Up @@ -644,7 +687,7 @@ describe(launchRushFrontendAsync.name, () => {
executeCurrentRush: (version, selectedRushLib, launchOptions) => {
void version;
void selectedRushLib;
emitCommandStarted(launchOptions.reporterEventSink);
emitCommandStarted(launchOptions.reporter.eventSink);
const parser: RushCommandLineParser = Object.create(RushCommandLineParser.prototype);
Object.defineProperty(parser, '_debugParameter', { value: { value: false } });
Object.defineProperty(parser, '_rushOptions', {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Expose an optional scoped reporter producer API to Rush actions and plugins while preserving legacy terminal output.",
"type": "patch"
}
],
"packageName": "@microsoft/rush",
"email": "TheLarkInn@users.noreply.github.com"
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "2f7908424d103b2f677e95bcd5d85a385b75eda2",
"pnpmShrinkwrapHash": "e3fd56b3094928b8856da3821af80ef4deee0529",
"preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9",
"packageJsonInjectedDependenciesHash": "e8fe4109038ad6e9b1e97cbb83e63d9094d37fe4"
"packageJsonInjectedDependenciesHash": "b0634100322878d7a992fa589326473bc3965ab6"
}
3 changes: 3 additions & 0 deletions common/config/subspaces/default/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,34 @@ import { AsyncSeriesWaterfallHook } from 'tapable';
import type { CollatedWriter } from '@rushstack/stream-collator';
import type { CommandLineParameter } from '@rushstack/ts-command-line';
import { CommandLineParameterKind } from '@rushstack/ts-command-line';
import { createRushDiagnostic } from '@rushstack/rush-reporter';
import { CredentialCache } from '@rushstack/credential-cache';
import { HookMap } from 'tapable';
import { ICreateRushDiagnosticOptions } from '@rushstack/rush-reporter';
import { ICredentialCacheEntry } from '@rushstack/credential-cache';
import { ICredentialCacheOptions } from '@rushstack/credential-cache';
import { IFileDiffStatus } from '@rushstack/package-deps-hash';
import { IPackageJson } from '@rushstack/node-core-library';
import { IPrefixMatch } from '@rushstack/lookup-by-path';
import type { IProblemCollector } from '@rushstack/terminal';
import { IReporterEventScope } from '@rushstack/rush-reporter';
import { IReporterEventSink } from '@rushstack/rush-reporter';
import { IRushDiagnostic } from '@rushstack/rush-reporter';
import { IScopedLogger } from '@rushstack/rush-reporter';
import { IScopedMessageOptions } from '@rushstack/rush-reporter';
import { IScopedReporter } from '@rushstack/rush-reporter';
import { ITerminal } from '@rushstack/terminal';
import type { ITerminalChunk } from '@rushstack/terminal';
import { ITerminalProvider } from '@rushstack/terminal';
import { JsonNull } from '@rushstack/node-core-library';
import { JsonObject } from '@rushstack/node-core-library';
import { LookupByPath } from '@rushstack/lookup-by-path';
import { PackageNameParser } from '@rushstack/node-core-library';
import { parseReporterExtensionEventName } from '@rushstack/rush-reporter';
import type { PerformanceEntry as PerformanceEntry_2 } from 'node:perf_hooks';
import { ReporterExtensionEventName } from '@rushstack/rush-reporter';
import { ReporterJsonValue } from '@rushstack/rush-reporter';
import { ReporterPrivacyClassification } from '@rushstack/rush-reporter';
import type { StdioSummarizer } from '@rushstack/terminal';
import { SyncHook } from 'tapable';
import { SyncWaterfallHook } from 'tapable';
Expand Down Expand Up @@ -148,6 +160,8 @@ export class CommonVersionsConfiguration {
saveAsync(): Promise<boolean>;
}

export { createRushDiagnostic }

export { CredentialCache }

// @beta
Expand Down Expand Up @@ -439,6 +453,8 @@ export interface ICreateOperationsContext {
readonly rushConfiguration: RushConfiguration;
}

export { ICreateRushDiagnosticOptions }

export { ICredentialCacheEntry }

export { ICredentialCacheOptions }
Expand Down Expand Up @@ -555,6 +571,8 @@ export interface ILaunchOptions {
// @internal
builtInPluginConfigurations?: _IBuiltInPluginConfiguration[];
isManaged: boolean;
// @internal
reporter?: IRushSessionReporterOptions;
terminalProvider?: ITerminalProvider;
}

Expand Down Expand Up @@ -909,6 +927,10 @@ export type _IProjectBuildCacheOptions = _IOperationBuildCacheOptions & {
phaseName: string;
};

export { IReporterEventScope }

export { IReporterEventSink }

// @beta
export interface IRushCommand {
readonly actionName: string;
Expand Down Expand Up @@ -941,6 +963,8 @@ export interface IRushCommandLineSpec {
// @beta (undocumented)
export type IRushConfigurationProjectForSnapshot = Pick<RushConfigurationProject, 'projectFolder' | 'projectRelativeFolder'>;

export { IRushDiagnostic }

// @alpha (undocumented)
export interface IRushPhaseSharding {
count: number;
Expand Down Expand Up @@ -981,10 +1005,23 @@ export interface IRushReportingConfiguration {
export interface IRushSessionOptions {
// (undocumented)
getIsDebugMode: () => boolean;
reporter?: IRushSessionReporterOptions;
// (undocumented)
terminalProvider: ITerminalProvider;
}

// @beta
export interface IRushSessionReporterOptions {
readonly eventSink: IReporterEventSink;
readonly sessionId: string;
}

export { IScopedLogger }

export { IScopedMessageOptions }

export { IScopedReporter }

// @beta
export interface IStopwatchResult {
get duration(): number;
Expand Down Expand Up @@ -1286,6 +1323,8 @@ export abstract class PackageManagerOptionsConfigurationBase implements IPackage
// @beta
export type Parallelism = number | IParallelismScalar;

export { parseReporterExtensionEventName }

// @alpha
export class PhasedCommandHooks {
readonly createOperationsAsync: AsyncSeriesWaterfallHook<[
Expand Down Expand Up @@ -1363,6 +1402,12 @@ export class ProjectChangeAnalyzer {
_tryGetSnapshotProviderAsync(projectConfigurations: ReadonlyMap<RushConfigurationProject, RushProjectConfiguration>, terminal: ITerminal, projectSelection?: ReadonlySet<RushConfigurationProject>): Promise<GetInputsSnapshotAsyncFn | undefined>;
}

export { ReporterExtensionEventName }

export { ReporterJsonValue }

export { ReporterPrivacyClassification }

// @public
export class RepoStateFile {
readonly filePath: string;
Expand Down Expand Up @@ -1700,6 +1745,8 @@ export class RushSession {
getCobuildLockProviderFactory(cobuildLockProviderName: string): CobuildLockProviderFactory | undefined;
// (undocumented)
getLogger(name: string): ILogger;
getReporter(scope?: IReporterEventScope): IScopedReporter | undefined;
getScopedLogger(scope?: IReporterEventScope): IScopedLogger | undefined;
// (undocumented)
readonly hooks: RushLifecycleHooks;
// (undocumented)
Expand Down
13 changes: 13 additions & 0 deletions libraries/rush-lib/src/api/Rush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { RushXCommandLine } from '../cli/RushXCommandLine';
import { CommandLineMigrationAdvisor } from '../cli/CommandLineMigrationAdvisor';
import { EnvironmentVariableNames } from './EnvironmentConfiguration';
import type { IBuiltInPluginConfiguration } from '../pluginFramework/PluginLoader/BuiltInPluginLoader';
import type { IRushSessionReporterOptions } from '../pluginFramework/RushSession';
import { RushPnpmCommandLine } from '../cli/RushPnpmCommandLine';
import { measureAsyncFn } from '../utilities/performance';

Expand Down Expand Up @@ -58,6 +59,17 @@ export interface ILaunchOptions {
* @internal
*/
builtInPluginConfigurations?: IBuiltInPluginConfiguration[];

/**
* Supplies the structured event sink owned by the Rush frontend.
*
* @remarks
* This is an internal cross-version frontend-to-engine handoff. Reporter
* selection and concrete reporter instances remain owned by the frontend.
*
* @internal
*/
reporter?: IRushSessionReporterOptions;
}

let _rushLibPackageJsonCache: IPackageJson | undefined = undefined;
Expand Down Expand Up @@ -98,6 +110,7 @@ export class Rush {
const parser: RushCommandLineParser = new RushCommandLineParser({
alreadyReportedNodeTooNewError: options.alreadyReportedNodeTooNewError,
builtInPluginConfigurations: options.builtInPluginConfigurations,
reporter: options.reporter,
reporterCloseAsync: frontendOptions.reporterCloseAsync
});
// CommandLineParser.executeAsync() should never reject the promise
Expand Down
Loading