diff --git a/common/changes/@microsoft/rush/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json b/common/changes/@microsoft/rush/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json new file mode 100644 index 00000000000..fb9e4abe7ba --- /dev/null +++ b/common/changes/@microsoft/rush/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Embed the generated zero-dependency Rush reporter bootstrap protocol in the install-run-rush bundle.", + "type": "patch" + } + ], + "packageName": "@microsoft/rush", + "email": "223556219+Copilot@users.noreply.github.com" +} diff --git a/common/changes/@rushstack/rush-reporter/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json b/common/changes/@rushstack/rush-reporter/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json new file mode 100644 index 00000000000..c1b94f1b98d --- /dev/null +++ b/common/changes/@rushstack/rush-reporter/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@rushstack/rush-reporter", + "comment": "Add the source-of-truth frozen bootstrap envelope encoder and deterministic generation check for install-run-rush.", + "type": "patch" + } + ], + "packageName": "@rushstack/rush-reporter", + "email": "223556219+Copilot@users.noreply.github.com" +} diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 30f755f0c56..76e3feb4d1d 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -4070,6 +4070,9 @@ importers: local-node-rig: specifier: workspace:* version: link:../../rigs/local-node-rig + typescript: + specifier: ~5.8.2 + version: 5.8.2 ../../../libraries/rig-package: dependencies: diff --git a/libraries/reporter/config/heft.json b/libraries/reporter/config/heft.json new file mode 100644 index 00000000000..1c9c3dd515e --- /dev/null +++ b/libraries/reporter/config/heft.json @@ -0,0 +1,24 @@ +/** + * Defines configuration used by core Heft. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + + "extends": "local-node-rig/profiles/default/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "check-bootstrap-protocol": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft", + "pluginName": "run-script-plugin", + "options": { + "scriptPath": "./scripts/generateBootstrapProtocol.js" + } + } + } + } + } + } +} diff --git a/libraries/reporter/package.json b/libraries/reporter/package.json index 0dac948f585..db09a0faafa 100644 --- a/libraries/reporter/package.json +++ b/libraries/reporter/package.json @@ -43,6 +43,8 @@ }, "scripts": { "build": "heft build --clean", + "generate-bootstrap-protocol": "node scripts/generateBootstrapProtocol.js --write", + "check-bootstrap-protocol": "node scripts/generateBootstrapProtocol.js --check", "_phase:build": "heft run --only build -- --clean", "_phase:test": "heft run --only test -- --clean" }, @@ -50,7 +52,8 @@ "@rushstack/heft": "workspace:*", "eslint": "~9.37.0", "local-node-rig": "workspace:*", - "@types/semver": "7.7.1" + "@types/semver": "7.7.1", + "typescript": "~5.8.2" }, "peerDependencies": { "@types/node": "*" diff --git a/libraries/reporter/scripts/generateBootstrapProtocol.d.ts b/libraries/reporter/scripts/generateBootstrapProtocol.d.ts new file mode 100644 index 00000000000..6938d89b10d --- /dev/null +++ b/libraries/reporter/scripts/generateBootstrapProtocol.d.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export declare function assertSelfContainedBootstrapSource(source: string): void; diff --git a/libraries/reporter/scripts/generateBootstrapProtocol.js b/libraries/reporter/scripts/generateBootstrapProtocol.js new file mode 100644 index 00000000000..01d27ee67f1 --- /dev/null +++ b/libraries/reporter/scripts/generateBootstrapProtocol.js @@ -0,0 +1,192 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const ts = require('typescript'); + +const SOURCE_START_MARKER = '// BEGIN GENERATED BOOTSTRAP PROTOCOL'; +const SOURCE_END_MARKER = '// END GENERATED BOOTSTRAP PROTOCOL'; +const SOURCE_PATH = path.resolve(__dirname, '../src/bootstrap/BootstrapProtocol.ts'); +const PROTOCOL_SOURCE_PATH = path.resolve(__dirname, '../src/protocol/ReporterProtocol.ts'); +const TARGET_PATH = path.resolve(__dirname, '../../rush-lib/src/scripts/generated/BootstrapProtocol.ts'); + +function unwrapExpression(expression) { + let current = expression; + while ( + ts.isParenthesizedExpression(current) || + ts.isAsExpression(current) || + ts.isTypeAssertionExpression(current) || + ts.isNonNullExpression(current) || + ts.isSatisfiesExpression(current) + ) { + current = current.expression; + } + return current; +} + +function isRequireRootedExpression(expression) { + let current = expression; + for (;;) { + current = unwrapExpression(current); + if (ts.isBinaryExpression(current) && current.operatorToken.kind === ts.SyntaxKind.CommaToken) { + current = current.right; + continue; + } + if (ts.isIdentifier(current)) { + return current.text === 'require'; + } + if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) { + current = current.expression; + continue; + } + return false; + } +} + +function getModuleEdgeKind(node) { + if (ts.isImportDeclaration(node)) { + return 'an import declaration'; + } + if (ts.isImportEqualsDeclaration(node)) { + return 'an import-equals declaration'; + } + if (ts.isImportTypeNode(node)) { + return 'an import type'; + } + if (ts.isExportDeclaration(node) && node.moduleSpecifier) { + return 'an export-from declaration'; + } + if (ts.isMetaProperty(node) && node.keywordToken === ts.SyntaxKind.ImportKeyword) { + return 'an import.meta expression'; + } + if (ts.isCallExpression(node)) { + if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { + return 'a dynamic import'; + } + if (isRequireRootedExpression(node.expression)) { + return 'a require-rooted call'; + } + } + if (ts.isNewExpression(node) && isRequireRootedExpression(node.expression)) { + return 'a require-rooted constructor'; + } + + return undefined; +} + +function assertSelfContainedBootstrapSource(source) { + const sourceFile = ts.createSourceFile( + 'BootstrapProtocol.generated.ts', + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS + ); + + function visit(node) { + const moduleEdgeKind = getModuleEdgeKind(node); + if (moduleEdgeKind) { + const location = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); + throw new Error( + `The generated bootstrap protocol must be self-contained; found ${moduleEdgeKind} at ` + + `line ${location.line + 1}, column ${location.character + 1}.` + ); + } + ts.forEachChild(node, visit); + } + + visit(sourceFile); +} + +function renderGeneratedFile() { + const source = fs.readFileSync(SOURCE_PATH, 'utf8').replace(/\r\n/g, '\n'); + const protocolSource = fs.readFileSync(PROTOCOL_SOURCE_PATH, 'utf8').replace(/\r\n/g, '\n'); + const startIndex = source.indexOf(SOURCE_START_MARKER); + const endIndex = source.indexOf(SOURCE_END_MARKER); + if (startIndex < 0 || endIndex < 0 || endIndex <= startIndex) { + throw new Error(`Unable to find the generated bootstrap protocol markers in ${SOURCE_PATH}.`); + } + + const generatedSource = source.slice(startIndex + SOURCE_START_MARKER.length, endIndex).trim(); + assertSelfContainedBootstrapSource(generatedSource); + + const bootstrapMajorMatch = generatedSource.match(/export const BOOTSTRAP_PROTOCOL_MAJOR: number = (\d+);/); + const reporterMajorMatch = protocolSource.match(/REPORTER_PROTOCOL_VERSION:[^=]+=\s*\{\s*major:\s*(\d+),/); + if (!bootstrapMajorMatch || !reporterMajorMatch) { + throw new Error('Unable to read the bootstrap and reporter protocol-major constants.'); + } + if (bootstrapMajorMatch[1] !== reporterMajorMatch[1]) { + throw new Error( + `BOOTSTRAP_PROTOCOL_MAJOR (${bootstrapMajorMatch[1]}) must match ` + + `REPORTER_PROTOCOL_VERSION.major (${reporterMajorMatch[1]}).` + ); + } + + return [ + '// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.', + '// See LICENSE in the project root for license information.', + '', + '// THIS FILE IS GENERATED. Run "rushx generate-bootstrap-protocol" in libraries/reporter to update it.', + '// Sources: libraries/reporter/src/bootstrap/BootstrapProtocol.ts', + '// libraries/reporter/src/protocol/ReporterProtocol.ts', + '', + generatedSource, + '' + ].join('\n'); +} + +function writeGeneratedFile() { + fs.mkdirSync(path.dirname(TARGET_PATH), { recursive: true }); + fs.writeFileSync(TARGET_PATH, renderGeneratedFile(), 'utf8'); +} + +function checkGeneratedFile() { + const expected = renderGeneratedFile(); + let actual; + try { + actual = fs.readFileSync(TARGET_PATH, 'utf8').replace(/\r\n/g, '\n'); + } catch (error) { + if (error && error.code === 'ENOENT') { + throw new Error( + `The generated bootstrap protocol is missing at ${TARGET_PATH}. ` + + 'Run "rushx generate-bootstrap-protocol" in libraries/reporter.' + ); + } + throw error; + } + + if (actual !== expected) { + throw new Error( + `The generated bootstrap protocol is stale at ${TARGET_PATH}. ` + + 'Run "rushx generate-bootstrap-protocol" in libraries/reporter.' + ); + } +} + +module.exports = { + assertSelfContainedBootstrapSource, + runAsync: async ({ + heftTaskSession: { + logger: { terminal } + } + }) => { + checkGeneratedFile(); + terminal.writeVerboseLine('The generated install-run-rush bootstrap protocol is up to date.'); + } +}; + +if (require.main === module) { + try { + const mode = process.argv[2]; + if (mode === '--write') { + writeGeneratedFile(); + } else if (mode === '--check') { + checkGeneratedFile(); + } else { + throw new Error('Specify either --write or --check.'); + } + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + } +} diff --git a/libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts b/libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts index 0b5846deda1..c3789cf8789 100644 --- a/libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts +++ b/libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts @@ -2,10 +2,10 @@ // See LICENSE in the project root for license information. import { - BOOTSTRAP_PROTOCOL_MAJOR, BOOTSTRAP_BUFFER_MAX_BYTES, BOOTSTRAP_EXTERNAL_CHUNK_MAX_BYTES, - BOOTSTRAP_BUFFER_TRUNCATED_EXTENSION_NAME + BOOTSTRAP_BUFFER_TRUNCATED_EXTENSION_NAME, + encodeBootstrapEnvelope } from './BootstrapProtocol'; import type { ReporterEventType } from '../events/ReporterEventType'; import { chunkUtf8Text } from '../utilities/chunkUtf8Text'; @@ -192,8 +192,7 @@ export class BootstrapEventBuffer { public emit(input: IBootstrapEventInput): string { const eventId: string = `boot_${this._nextEventId++}`; const required: boolean = input.type !== 'activityChanged'; - const envelope: Record = { - protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 }, + const line: string = encodeBootstrapEnvelope({ eventId, sessionId: this._sessionId, sequence: this._nextSequence++, @@ -203,8 +202,7 @@ export class BootstrapEventBuffer { required, type: input.type, payload: input.payload === undefined ? {} : input.payload - }; - const line: string = JSON.stringify(envelope); + }); const bytes: number = Buffer.byteLength(line, 'utf8') + 1; const mustPreserve: boolean = required; const replaceable: boolean = input.type === 'activityChanged'; @@ -257,8 +255,7 @@ export class BootstrapEventBuffer { public serialize(): string { const lines: string[] = this._entries.map((entry: IBufferEntry) => entry.line); if (this._truncated) { - const notice: Record = { - protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 }, + const noticeLine: string = encodeBootstrapEnvelope({ eventId: 'boot_bufferTruncated', sessionId: this._sessionId, sequence: this._nextSequence++, @@ -274,8 +271,7 @@ export class BootstrapEventBuffer { droppedRequired: this._droppedRequired, failed: this._failed } - }; - const noticeLine: string = JSON.stringify(notice); + }); const noticeBytes: number = Buffer.byteLength(noticeLine, 'utf8') + 1; if (noticeBytes > TRUNCATION_NOTICE_RESERVE_BYTES) { throw new Error('The bootstrap truncation notice exceeded its reserved capacity.'); diff --git a/libraries/reporter/src/bootstrap/BootstrapProtocol.ts b/libraries/reporter/src/bootstrap/BootstrapProtocol.ts index 4672d239685..84b72c09b78 100644 --- a/libraries/reporter/src/bootstrap/BootstrapProtocol.ts +++ b/libraries/reporter/src/bootstrap/BootstrapProtocol.ts @@ -6,18 +6,76 @@ // zero-dependency `install-run-rush` bundle, which must not import // `@rushstack/rush-reporter` at runtime. +// BEGIN GENERATED BOOTSTRAP PROTOCOL + /** * The protocol major version frozen into the bootstrap encoder. * * @remarks - * This constant is generated from `@rushstack/rush-reporter` and must equal - * `REPORTER_PROTOCOL_VERSION.major`. It is duplicated here, rather than - * imported, so the encoder can be embedded without a runtime dependency. + * The `install-run-rush` build embeds a generated copy of this constant and + * the encoder below. The generated module is checked byte-for-byte during the + * reporter build. * * @beta */ export const BOOTSTRAP_PROTOCOL_MAJOR: number = 1; +/** + * The privacy classification accepted by the frozen bootstrap encoder. + * + * @beta + */ +export type BootstrapEnvelopePrivacyClassification = 'public' | 'local-sensitive' | 'secret'; + +/** + * The producer identity stamped onto a bootstrap event. + * + * @beta + */ +export interface IBootstrapEnvelopeSource { + readonly packageName: string; + readonly packageVersion: string; +} + +/** + * The presentation-free fields encoded into a bootstrap event envelope. + * + * @beta + */ +export interface IBootstrapEnvelopeInput { + readonly eventId: string; + readonly sessionId: string; + readonly sequence: number; + readonly timestamp: string; + readonly source: IBootstrapEnvelopeSource; + readonly privacy: BootstrapEnvelopePrivacyClassification; + readonly required: boolean; + readonly type: string; + readonly payload: unknown; +} + +/** + * Encodes one bootstrap event envelope without importing the reporter package. + * + * @beta + */ +export function encodeBootstrapEnvelope(input: IBootstrapEnvelopeInput): string { + return JSON.stringify({ + protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 }, + eventId: input.eventId, + sessionId: input.sessionId, + sequence: input.sequence, + timestamp: input.timestamp, + source: input.source, + privacy: input.privacy, + required: input.required, + type: input.type, + payload: input.payload === undefined ? {} : input.payload + }); +} + +// END GENERATED BOOTSTRAP PROTOCOL + /** * The maximum size of the buffered bootstrap event stream, in bytes (1 MiB). * diff --git a/libraries/reporter/src/test/Bootstrap.test.ts b/libraries/reporter/src/test/Bootstrap.test.ts index 30cf44b510f..7755364d49d 100644 --- a/libraries/reporter/src/test/Bootstrap.test.ts +++ b/libraries/reporter/src/test/Bootstrap.test.ts @@ -5,6 +5,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; +import { assertSelfContainedBootstrapSource } from '../../scripts/generateBootstrapProtocol'; import { parseEarlyReporterControls, BootstrapEventBuffer, @@ -19,6 +20,7 @@ import { type IBootstrapEventBufferOptions, type IEarlyReporterControls } from '../index'; +import { encodeBootstrapEnvelope } from '../bootstrap/BootstrapProtocol'; function decode(ndjson: string): Record[] { return ndjson @@ -37,6 +39,40 @@ function makeBuffer(overrides?: Partial): Bootstra }); } +describe('bootstrap protocol generation', () => { + it.each([ + { description: 'static imports', source: "import { value } from 'pkg';" }, + { description: 'import-equals declarations', source: "import value = require('pkg');" }, + { description: 'import types', source: "type Value = import('pkg').Value;" }, + { description: 'dynamic imports', source: "const value = import('pkg');" }, + { description: 'export-from declarations', source: "export { value } from 'pkg';" }, + { description: 'export-all declarations', source: "export * from 'pkg';" }, + { description: 'import.meta expressions', source: 'const url = import.meta.url;' }, + { description: 'require calls', source: "const value = require('pkg');" }, + { description: 'require property calls', source: "const path = require.resolve('pkg');" }, + { description: 'parenthesized require calls', source: "const value = (require)('pkg');" }, + { description: 'require element-access calls', source: "const path = require['resolve']('pkg');" }, + { description: 'nested require property calls', source: "const paths = require.resolve.paths('pkg');" }, + { description: 'require constructors', source: "const value = new require('pkg');" }, + { description: 'parenthesized require constructors', source: "const value = new (require)('pkg');" }, + { description: 'comma-expression require calls', source: "const value = (0, require)('pkg');" } + ])('rejects $description', ({ source }: { source: string }) => { + expect(() => assertSelfContainedBootstrapSource(source)).toThrow( + 'The generated bootstrap protocol must be self-contained' + ); + }); + + it('allows import-like text without module edges', () => { + const source: string = [ + "/* import { value } from 'pkg'; */", + `const message: string = "import('pkg')";`, + 'export const value: string = message;' + ].join('\n'); + + expect(() => assertSelfContainedBootstrapSource(source)).not.toThrow(); + }); +}); + describe('parseEarlyReporterControls', () => { it('reads the reporter and log level from flags', () => { const controls: IEarlyReporterControls = parseEarlyReporterControls( @@ -74,6 +110,46 @@ describe('BootstrapEventBuffer', () => { expect(BOOTSTRAP_PROTOCOL_MAJOR).toBe(REPORTER_PROTOCOL_VERSION.major); }); + it('encodes the frozen bootstrap envelope deterministically', () => { + expect( + encodeBootstrapEnvelope({ + eventId: 'boot_1', + sessionId: 'sess_boot', + sequence: 1, + timestamp: '2026-01-01T00:00:00.000Z', + source: { packageName: 'install-run-rush', packageVersion: '0.0.0' }, + privacy: 'public', + required: true, + type: 'sessionStarted', + payload: { argv: ['build'] } + }) + ).toBe( + '{"protocolVersion":{"major":1,"minor":0},"eventId":"boot_1","sessionId":"sess_boot",' + + '"sequence":1,"timestamp":"2026-01-01T00:00:00.000Z","source":{"packageName":' + + '"install-run-rush","packageVersion":"0.0.0"},"privacy":"public","required":true,' + + '"type":"sessionStarted","payload":{"argv":["build"]}}' + ); + }); + + it('preserves the payload field when the input payload is undefined', () => { + const envelope: Record = JSON.parse( + encodeBootstrapEnvelope({ + eventId: 'boot_1', + sessionId: 'sess_boot', + sequence: 1, + timestamp: '2026-01-01T00:00:00.000Z', + source: { packageName: 'install-run-rush', packageVersion: '0.0.0' }, + privacy: 'public', + required: true, + type: 'sessionStarted', + payload: undefined + }) + ) as Record; + + expect(Object.hasOwn(envelope, 'payload')).toBe(true); + expect(envelope.payload).toEqual({}); + }); + it('encodes events with assigned ids, sequence, timestamp, and protocol version', () => { const buffer: BootstrapEventBuffer = makeBuffer(); const id: string = buffer.emit({ type: 'sessionStarted', payload: { argv: ['build'] } }); diff --git a/libraries/rush-lib/config/heft.json b/libraries/rush-lib/config/heft.json index 02a4934f2bf..0b296d4499c 100644 --- a/libraries/rush-lib/config/heft.json +++ b/libraries/rush-lib/config/heft.json @@ -12,6 +12,16 @@ "cleanFiles": [{ "includeGlobs": ["lib-intermediate-commonjs", "lib-intermediate-esm"] }], "tasksByName": { + "check-bootstrap-protocol": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft", + "pluginName": "run-script-plugin", + "options": { + "scriptPath": "../reporter/scripts/generateBootstrapProtocol.js" + } + } + }, + "copy-mock-flush-telemetry-plugin": { "taskDependencies": ["typescript"], "taskPlugin": { diff --git a/libraries/rush-lib/src/scripts/generated/BootstrapProtocol.ts b/libraries/rush-lib/src/scripts/generated/BootstrapProtocol.ts new file mode 100644 index 00000000000..97bfe28545b --- /dev/null +++ b/libraries/rush-lib/src/scripts/generated/BootstrapProtocol.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// THIS FILE IS GENERATED. Run "rushx generate-bootstrap-protocol" in libraries/reporter to update it. +// Sources: libraries/reporter/src/bootstrap/BootstrapProtocol.ts +// libraries/reporter/src/protocol/ReporterProtocol.ts + +/** + * The protocol major version frozen into the bootstrap encoder. + * + * @remarks + * The `install-run-rush` build embeds a generated copy of this constant and + * the encoder below. The generated module is checked byte-for-byte during the + * reporter build. + * + * @beta + */ +export const BOOTSTRAP_PROTOCOL_MAJOR: number = 1; + +/** + * The privacy classification accepted by the frozen bootstrap encoder. + * + * @beta + */ +export type BootstrapEnvelopePrivacyClassification = 'public' | 'local-sensitive' | 'secret'; + +/** + * The producer identity stamped onto a bootstrap event. + * + * @beta + */ +export interface IBootstrapEnvelopeSource { + readonly packageName: string; + readonly packageVersion: string; +} + +/** + * The presentation-free fields encoded into a bootstrap event envelope. + * + * @beta + */ +export interface IBootstrapEnvelopeInput { + readonly eventId: string; + readonly sessionId: string; + readonly sequence: number; + readonly timestamp: string; + readonly source: IBootstrapEnvelopeSource; + readonly privacy: BootstrapEnvelopePrivacyClassification; + readonly required: boolean; + readonly type: string; + readonly payload: unknown; +} + +/** + * Encodes one bootstrap event envelope without importing the reporter package. + * + * @beta + */ +export function encodeBootstrapEnvelope(input: IBootstrapEnvelopeInput): string { + return JSON.stringify({ + protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 }, + eventId: input.eventId, + sessionId: input.sessionId, + sequence: input.sequence, + timestamp: input.timestamp, + source: input.source, + privacy: input.privacy, + required: input.required, + type: input.type, + payload: input.payload === undefined ? {} : input.payload + }); +} diff --git a/libraries/rush-lib/src/scripts/install-run-rush.ts b/libraries/rush-lib/src/scripts/install-run-rush.ts index 6fa7e8b21b5..1bb7b29d0c5 100644 --- a/libraries/rush-lib/src/scripts/install-run-rush.ts +++ b/libraries/rush-lib/src/scripts/install-run-rush.ts @@ -6,13 +6,15 @@ import * as path from 'node:path'; import * as fs from 'node:fs'; +import type { ILogger } from '../utilities/npmrcUtilities'; +import { BOOTSTRAP_PROTOCOL_MAJOR, encodeBootstrapEnvelope } from './generated/BootstrapProtocol'; + const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode }: typeof import('./install-run') = __non_webpack_require__('./install-run'); -import type { ILogger } from '../utilities/npmrcUtilities'; const PACKAGE_NAME: string = '@microsoft/rush'; const RUSH_PREVIEW_VERSION: string = 'RUSH_PREVIEW_VERSION'; @@ -20,6 +22,12 @@ const RUSH_QUIET_MODE: string = 'RUSH_QUIET_MODE'; const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE: 'INSTALL_RUN_RUSH_LOCKFILE_PATH' = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; +function _validateBundledBootstrapProtocol(): void { + if (BOOTSTRAP_PROTOCOL_MAJOR < 1 || typeof encodeBootstrapEnvelope !== 'function') { + throw new Error('The bundled Rush reporter bootstrap protocol is invalid.'); + } +} + function _getRushVersion(logger: ILogger): string { const rushPreviewVersion: string | undefined = process.env[RUSH_PREVIEW_VERSION]; if (rushPreviewVersion !== undefined) { @@ -58,6 +66,8 @@ function _getBin(scriptName: string): string { } function _run(): void { + _validateBundledBootstrapProtocol(); + const [ nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */,