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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,14 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/libraries/package-extractor](./libraries/package-extractor/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fpackage-extractor.svg)](https://badge.fury.io/js/%40rushstack%2Fpackage-extractor) | [changelog](./libraries/package-extractor/CHANGELOG.md) | [@rushstack/package-extractor](https://www.npmjs.com/package/@rushstack/package-extractor) |
| [/libraries/problem-matcher](./libraries/problem-matcher/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fproblem-matcher.svg)](https://badge.fury.io/js/%40rushstack%2Fproblem-matcher) | [changelog](./libraries/problem-matcher/CHANGELOG.md) | [@rushstack/problem-matcher](https://www.npmjs.com/package/@rushstack/problem-matcher) |
| [/libraries/rig-package](./libraries/rig-package/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frig-package.svg)](https://badge.fury.io/js/%40rushstack%2Frig-package) | [changelog](./libraries/rig-package/CHANGELOG.md) | [@rushstack/rig-package](https://www.npmjs.com/package/@rushstack/rig-package) |
| [/libraries/rush-daemon-protocol](./libraries/rush-daemon-protocol/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-daemon-protocol.svg)](https://badge.fury.io/js/%40rushstack%2Frush-daemon-protocol) | [changelog](./libraries/rush-daemon-protocol/CHANGELOG.md) | [@rushstack/rush-daemon-protocol](https://www.npmjs.com/package/@rushstack/rush-daemon-protocol) |
| [/libraries/rush-daemon-transport](./libraries/rush-daemon-transport/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-daemon-transport.svg)](https://badge.fury.io/js/%40rushstack%2Frush-daemon-transport) | [changelog](./libraries/rush-daemon-transport/CHANGELOG.md) | [@rushstack/rush-daemon-transport](https://www.npmjs.com/package/@rushstack/rush-daemon-transport) |
| [/libraries/rush-lib](./libraries/rush-lib/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Frush-lib.svg)](https://badge.fury.io/js/%40microsoft%2Frush-lib) | | [@microsoft/rush-lib](https://www.npmjs.com/package/@microsoft/rush-lib) |
| [/libraries/rush-pnpm-kit-v10](./libraries/rush-pnpm-kit-v10/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v10.svg)](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v10) | [changelog](./libraries/rush-pnpm-kit-v10/CHANGELOG.md) | [@rushstack/rush-pnpm-kit-v10](https://www.npmjs.com/package/@rushstack/rush-pnpm-kit-v10) |
| [/libraries/rush-pnpm-kit-v8](./libraries/rush-pnpm-kit-v8/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v8.svg)](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v8) | [changelog](./libraries/rush-pnpm-kit-v8/CHANGELOG.md) | [@rushstack/rush-pnpm-kit-v8](https://www.npmjs.com/package/@rushstack/rush-pnpm-kit-v8) |
| [/libraries/rush-pnpm-kit-v9](./libraries/rush-pnpm-kit-v9/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v9.svg)](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v9) | [changelog](./libraries/rush-pnpm-kit-v9/CHANGELOG.md) | [@rushstack/rush-pnpm-kit-v9](https://www.npmjs.com/package/@rushstack/rush-pnpm-kit-v9) |
| [/libraries/rush-sdk](./libraries/rush-sdk/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-sdk.svg)](https://badge.fury.io/js/%40rushstack%2Frush-sdk) | | [@rushstack/rush-sdk](https://www.npmjs.com/package/@rushstack/rush-sdk) |
| [/libraries/rush-terminal-renderer](./libraries/rush-terminal-renderer/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-terminal-renderer.svg)](https://badge.fury.io/js/%40rushstack%2Frush-terminal-renderer) | [changelog](./libraries/rush-terminal-renderer/CHANGELOG.md) | [@rushstack/rush-terminal-renderer](https://www.npmjs.com/package/@rushstack/rush-terminal-renderer) |
| [/libraries/stream-collator](./libraries/stream-collator/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fstream-collator.svg)](https://badge.fury.io/js/%40rushstack%2Fstream-collator) | [changelog](./libraries/stream-collator/CHANGELOG.md) | [@rushstack/stream-collator](https://www.npmjs.com/package/@rushstack/stream-collator) |
| [/libraries/terminal](./libraries/terminal/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fterminal.svg)](https://badge.fury.io/js/%40rushstack%2Fterminal) | [changelog](./libraries/terminal/CHANGELOG.md) | [@rushstack/terminal](https://www.npmjs.com/package/@rushstack/terminal) |
| [/libraries/tree-pattern](./libraries/tree-pattern/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Ftree-pattern.svg)](https://badge.fury.io/js/%40rushstack%2Ftree-pattern) | [changelog](./libraries/tree-pattern/CHANGELOG.md) | [@rushstack/tree-pattern](https://www.npmjs.com/package/@rushstack/tree-pattern) |
Expand Down Expand Up @@ -226,6 +229,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/build-tests/rush-package-manager-integration-test](./build-tests/rush-package-manager-integration-test/) | Integration tests for non-pnpm package managers in Rush. |
| [/build-tests/rush-project-change-analyzer-test](./build-tests/rush-project-change-analyzer-test/) | This is an example project that uses rush-lib's ProjectChangeAnalyzer to |
| [/build-tests/rush-redis-cobuild-plugin-integration-test](./build-tests/rush-redis-cobuild-plugin-integration-test/) | Tests connecting to an redis server |
| [/build-tests/rushd-wire-e2e-test](./build-tests/rushd-wire-e2e-test/) | End-to-end conformance tests for the rushd wire layer (protocol + transport + renderer against the rush-lib engine) |
| [/build-tests/set-webpack-public-path-plugin-test](./build-tests/set-webpack-public-path-plugin-test/) | Building this project tests the set-webpack-public-path-plugin |
| [/build-tests/webpack-local-version-test](./build-tests/webpack-local-version-test/) | Building this project tests the rig loading for the local version of webpack |
| [/eslint/local-eslint-config](./eslint/local-eslint-config/) | An ESLint configuration consumed projects inside the rushstack repo. |
Expand Down
51 changes: 51 additions & 0 deletions build-tests/rushd-wire-e2e-test/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Agent coding contract — `rushd-wire-e2e-test`

This package is governed by an **ultra-strict lint policy** for generated code. All of the
rules below are enabled to `error` in `eslint.config.js` via the shared
`local-node-rig/profiles/default/includes/eslint/flat/mixins/strict-codegen.js` mixin.
They apply to **all** TypeScript in this package, **including tests** (`src/**/*.test.ts`).

## Enforced rules (do not attempt to bypass)

| Rule | Setting |
| ---- | ------- |
| `complexity` | `['error', 3]` |
| `max-depth` | `['error', 3]` |
| `max-lines-per-function` | `['error', 30]` |
| `max-lines` | `['error', 100]` — every file, including this means: keep files small; split modules |
| `max-params` | `['error', 4]` — use options objects |
| `@typescript-eslint/no-magic-numbers` | `'error'` — every numeric literal must be a named constant |
| `@typescript-eslint/prefer-nullish-coalescing` | `'error'` — use `??`, not `\|\|` or nullish-guard ternaries |
| `import/enforce-node-protocol-usage` | `['error', 'always']` — write `node:crypto`, never `crypto` |
| `import/order` | `['error', { alphabetize: asc, grouped, newlines-between: always }]` |
| `sort-imports` | `['error', { ignoreDeclarationSort: true }]` — sort named members |
| `@typescript-eslint/consistent-type-imports` | `['error', { fixStyle: 'separate-type-imports' }]` — `import type { X }`, never inline `type` specifiers |
| `import/no-relative-parent-imports` | `'error'` for non-test source — no `../` imports outside tests |
| `no-eval`, `@typescript-eslint/no-implied-eval` | `'error'` |

## Suppression is forbidden — mechanically enforced

- `linterOptions.noInlineConfig: true` makes **every** `eslint-disable*` comment a lint error.
- `reportUnusedDisableDirectives: 'error'` flags stale suppressions.
- Therefore, as an agent working in this package you MUST NOT:
- add `eslint-disable`, `eslint-disable-next-line`, `eslint-env`, or inline `/* eslint ... */` config comments;
- add entries to any `.eslint-bulk-suppressions.json`;
- add `eslintIgnore` keys to `package.json`;
- add `@ts-nocheck` or `@ts-ignore` comments;
- weaken, reorder, or remove the `strict-codegen` mixin in `eslint.config.js`.
- If a rule fires, **fix the code** (extract a constant, split the function/module, restructure) — never silence it.

## Deferred rules (do not emulate with hacks)

The following intended rules have no existing implementation in this repository's ESLint
toolchain and are **not yet enabled** (the user will wire them up later):
`no-magic-strings`, `no-object-mutation`, `no-array-mutation`,
`no-placeholder-implementation`, and the custom zero-tolerance import rules
(`no-re-export`, `require-clean-barrel`, `require-barrel-relative-exports`,
`no-export-alias`, `no-dynamic-import`, `no-hardcoded-secrets`,
`no-parent-internal-access`). Write code that would already satisfy them: prefer immutable
update patterns and named string constants, and never land stubs or `TODO` implementations.

## Design notes for this package

- Test-only project: adapts the rush-lib engine's dual-emit into wire frames and proves conformance (golden parity, raw streams, verbosity isolation, backpressure) through the real protocol and transport packages.
3 changes: 3 additions & 0 deletions build-tests/rushd-wire-e2e-test/config/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "local-node-rig/profiles/default/config/jest.config.json"
}
7 changes: 7 additions & 0 deletions build-tests/rushd-wire-e2e-test/config/rig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",

"rigPackageName": "local-node-rig"
}
23 changes: 23 additions & 0 deletions build-tests/rushd-wire-e2e-test/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

const nodeProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node');
const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals');
const tsdocMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/tsdoc');
const strictCodegenMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/strict-codegen');

module.exports = [
...nodeProfile,
...friendlyLocalsMixin,
...tsdocMixin,
// IMPORTANT: The strict-codegen mixin must remain last so its rules win conflicts.
...strictCodegenMixin,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
tsconfigRootDir: __dirname
}
}
}
];
23 changes: 23 additions & 0 deletions build-tests/rushd-wire-e2e-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "rushd-wire-e2e-test",
"description": "End-to-end conformance tests for the rushd wire layer (protocol + transport + renderer against the rush-lib engine)",
"version": "1.0.0",
"private": true,
"license": "MIT",
"scripts": {
"build": "heft build --clean",
"_phase:build": "heft run --only build -- --clean",
"_phase:test": "heft run --only test -- --clean"
},
"devDependencies": {
"@microsoft/rush-lib": "workspace:*",
"@rushstack/heft": "workspace:*",
"@rushstack/node-core-library": "workspace:*",
"@rushstack/rush-daemon-protocol": "workspace:*",
"@rushstack/rush-daemon-transport": "workspace:*",
"@rushstack/rush-terminal-renderer": "workspace:*",
"@rushstack/terminal": "workspace:*",
"eslint": "~9.37.0",
"local-node-rig": "workspace:*"
}
}
10 changes: 10 additions & 0 deletions build-tests/rushd-wire-e2e-test/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

/**
* End-to-end conformance tests for the rushd wire layer. See `src/test/`.
*
* @packageDocumentation
*/

export {};
57 changes: 57 additions & 0 deletions build-tests/rushd-wire-e2e-test/src/test/EngineRunners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

// Inline operation runners that use runWithTerminalAsync, matching the
// production runners (ShellOperationRunner, IPCOperationRunner).

import * as os from 'node:os';

import type { RushConfigurationProject } from '@microsoft/rush-lib/lib/api/RushConfigurationProject';
import type {
IOperationRunner,
IOperationRunnerContext
} from '@microsoft/rush-lib/lib/logic/operations/IOperationRunner';
import { Operation } from '@microsoft/rush-lib/lib/logic/operations/Operation';
import type { OperationStatus } from '@microsoft/rush-lib/lib/logic/operations/OperationStatus';

function createRunner(name: string, status: OperationStatus): IOperationRunner {
return {
name,
reportTiming: true,
silent: false,
cacheable: false,
warningsAreAllowed: false,
isNoOp: false,
executeAsync: async (context: IOperationRunnerContext) =>
await context.runWithTerminalAsync(
async (terminal) => {
terminal.writeLine(`${name}-out ünïcode ✓`);
terminal.writeErrorLine(`${name}-err`);
return status;
},
{ createLogFile: false, logFileSuffix: '' }
),
getConfigHash: () => 'e2e'
};
}

/** Creates a fixture operation writing deterministic unicode output. */
export function createScenarioOperation(name: string, status: OperationStatus): Operation {
return new Operation({
runner: createRunner(name, status),
logFilenameIdentifier: name,
phase: {
name: 'phase',
allowWarningsOnSuccess: false,
associatedParameters: new Set(),
dependencies: { self: new Set(), upstream: new Set() },
isSynthetic: false,
logFilenameIdentifier: 'phase',
missingScriptBehavior: 'silent'
},
project: {
packageName: name,
projectFolder: os.tmpdir()
} as unknown as RushConfigurationProject
});
}
60 changes: 60 additions & 0 deletions build-tests/rushd-wire-e2e-test/src/test/EngineScenario.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

// Builds and runs a small rush-lib operation graph with the dual-emit sink
// attached, capturing both the legacy terminal output and the wire frames.

import { OperationGraph } from '@microsoft/rush-lib/lib/logic/operations/OperationGraph';
import type { IOperationGraphOptions } from '@microsoft/rush-lib/lib/logic/operations/OperationGraph';
import { OperationStatus } from '@microsoft/rush-lib/lib/logic/operations/OperationStatus';

import { createScenarioOperation } from './EngineRunners';
import { TestWritable } from './TestWritable';
import { WireAdapter } from './WireAdapter';

const PARALLELISM: number = 1;

/** Options for {@link runEngineScenarioAsync}. */
export interface IEngineScenarioOptions {
/** Run the engine in quiet mode (stdout discarded from the collated terminal). */
readonly quiet: boolean;
/** Make the `beta` operation fail. */
readonly failing?: boolean;
}

/** The captured result of one engine run. */
export interface IEngineScenarioResult {
/** The legacy terminal output (golden reference). */
readonly writable: TestWritable;
/** The wire frames produced by the dual-emit sink. */
readonly adapter: WireAdapter;
}

/** Runs the fixture graph to completion with the wire adapter attached. */
export async function runEngineScenarioAsync(
options: IEngineScenarioOptions
): Promise<IEngineScenarioResult> {
const writable: TestWritable = new TestWritable();
const adapter: WireAdapter = new WireAdapter();
const graphOptions: IOperationGraphOptions = {
quietMode: options.quiet,
debugMode: false,
parallelism: PARALLELISM,
allowOversubscription: true,
destinations: [writable],
abortController: new AbortController()
};
const graph: OperationGraph = new OperationGraph(
new Set([
createScenarioOperation('alpha', OperationStatus.Success),
createScenarioOperation(
'beta',
options.failing ? OperationStatus.Failure : OperationStatus.Success
)
]),
graphOptions
);
graph.eventSink = adapter;
await graph.executeAsync({});
return { writable, adapter };
}
43 changes: 43 additions & 0 deletions build-tests/rushd-wire-e2e-test/src/test/FrameDispatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

// Shared client-side frame dispatch: decoded frames into a renderer host.

import { DaemonFrameType, decodeDaemonEventFrame, decodeDaemonLogChunk } from '@rushstack/rush-daemon-protocol';
import type { IDaemonFrame, IDaemonLogChunk } from '@rushstack/rush-daemon-protocol';
import type { DaemonRendererHost } from '@rushstack/rush-terminal-renderer';

const WIRE_DECODER: InstanceType<typeof TextDecoder> = new TextDecoder();

/** Returns true for `0x02`/`0x03` log frames. */
export function isLogFrame(frame: IDaemonFrame): boolean {
return frame.kind === DaemonFrameType.logStdout || frame.kind === DaemonFrameType.logStderr;
}

/** Maps a log frame type to its stream name. */
export function toStream(frame: IDaemonFrame): 'stdout' | 'stderr' {
return frame.kind === DaemonFrameType.logStderr ? 'stderr' : 'stdout';
}

/** Routes one decoded frame into the renderer host. */
export function dispatchFrame(host: DaemonRendererHost, frame: IDaemonFrame): void {
if (frame.kind === DaemonFrameType.event) {
host.handleEvent(decodeDaemonEventFrame(frame.payload));
return;
}
if (isLogFrame(frame)) {
const log: IDaemonLogChunk = decodeDaemonLogChunk(frame.payload);
host.handleLogChunk(log.operationId, toStream(frame), log.chunk);
}
}

/** Collects log frame payloads into per-operation ordered text chunks. */
export function collectLogChunk(perOperation: Map<string, string[]>, frame: IDaemonFrame): void {
if (!isLogFrame(frame)) {
return;
}
const log: IDaemonLogChunk = decodeDaemonLogChunk(frame.payload);
const chunks: string[] = perOperation.get(log.operationId) ?? [];
chunks.push(WIRE_DECODER.decode(log.chunk));
perOperation.set(log.operationId, chunks);
}
Loading