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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
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.

24 changes: 24 additions & 0 deletions libraries/reporter/config/heft.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
}
5 changes: 4 additions & 1 deletion libraries/reporter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,17 @@
},
"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"
},
"devDependencies": {
"@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": "*"
Expand Down
4 changes: 4 additions & 0 deletions libraries/reporter/scripts/generateBootstrapProtocol.d.ts
Original file line number Diff line number Diff line change
@@ -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;
192 changes: 192 additions & 0 deletions libraries/reporter/scripts/generateBootstrapProtocol.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
16 changes: 6 additions & 10 deletions libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string, unknown> = {
protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 },
const line: string = encodeBootstrapEnvelope({
eventId,
sessionId: this._sessionId,
sequence: this._nextSequence++,
Expand All @@ -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';
Expand Down Expand Up @@ -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<string, unknown> = {
protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 },
const noticeLine: string = encodeBootstrapEnvelope({
eventId: 'boot_bufferTruncated',
sessionId: this._sessionId,
sequence: this._nextSequence++,
Expand All @@ -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.');
Expand Down
Loading