Skip to content

Commit 915e134

Browse files
TheLarkInnCopilot
andcommitted
Merge remote-tracking branch 'origin/main' into thelarkinn-dogfood-rush-daemon
main (#6090) now keeps rush-client from loading @microsoft/rush-lib with on-demand requires and lazy start-command resolution. Take that implementation for the client and drop this branch's overlapping client-only modules (RushJsonLocation, RushXCommandLineArguments, DaemonLaunchCommand and bundledRushVersion) and their change files. The daemon-side changes in this branch are unaffected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2 parents 531fd51 + 1f51ac8 commit 915e134

37 files changed

Lines changed: 1168 additions & 331 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
#!/usr/bin/env node
2+
// Node >= 22.1 reuses compiled code across invocations; this does not change the environment.
3+
require('node:module').enableCompileCache?.();
24
require('../lib-commonjs/start.js');
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
#!/usr/bin/env node
2+
// Node >= 22.1 reuses compiled code across invocations; this does not change the environment.
3+
require('node:module').enableCompileCache?.();
24
require('../lib-commonjs/startRushX.js');

‎apps/rush-cli-client/src/bundledRushVersion.ts‎

Lines changed: 0 additions & 11 deletions
This file was deleted.

‎apps/rush-cli-client/src/daemonConnectionOptions.ts‎

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,9 @@ import { JsonFile } from '@rushstack/node-core-library';
77
import type { IConnectOrStartDaemonOptions } from '@rushstack/rush-client-core';
88
import { computeDaemonWorkspaceKey, resolveDaemonPathsFromProcess } from '@rushstack/rush-daemon-transport';
99
import { readDaemonInstallationMetadata } from '@rushstack/rush-daemon/lib/DaemonInstallation';
10-
import {
11-
DaemonLauncherUnavailableError,
12-
getSelectedDaemonStartCommand
13-
} from '@rushstack/rush-daemon/lib/DaemonLaunchCommand';
14-
import type { IVersionSelectedDaemonLaunch } from '@rushstack/rush-daemon/lib/VersionSelectedDaemonLauncher';
10+
import type * as VersionSelectedDaemonLauncherModule from '@rushstack/rush-daemon/lib/VersionSelectedDaemonLauncher';
1511

16-
import { BUNDLED_RUSH_VERSION } from './bundledRushVersion';
12+
import { getBundledRushVersion, loadVersionSelectedDaemonLauncher } from './lazyRushModules';
1713

1814
export function getDaemonConnectionOptions(
1915
repoRoot: string,
@@ -25,7 +21,7 @@ export function getDaemonConnectionOptions(
2521
const daemonPackagePath: string = require.resolve('@rushstack/rush-daemon/package.json');
2622
const daemonPackage: { version: string; bin: { rushd: string } } = JsonFile.load(daemonPackagePath);
2723
if (autoStart && readDaemonInstallationMetadata(daemonPackagePath).rushVersion !== rushVersion) {
28-
throw new DaemonLauncherUnavailableError(
24+
throw new (loadVersionSelectedDaemonLauncher().DaemonLauncherUnavailableError)(
2925
rushVersion,
3026
'The synchronous launcher only supports its installed engine; use asynchronous version selection.'
3127
);
@@ -34,7 +30,7 @@ export function getDaemonConnectionOptions(
3430
paths: resolveDaemonPathsFromProcess(computeDaemonWorkspaceKey({ canonicalRepoRoot, rushVersion })),
3531
expectedDaemonVersion: daemonPackage.version,
3632
startCommand: autoStart
37-
? getSelectedDaemonStartCommand(daemonPackagePath, {
33+
? loadVersionSelectedDaemonLauncher().getSelectedDaemonStartCommand(daemonPackagePath, {
3834
repoRoot: canonicalRepoRoot,
3935
rushVersion,
4036
environment
@@ -57,17 +53,25 @@ export async function getDaemonConnectionOptionsAsync(
5753
false
5854
);
5955
if (!autoStart) return { paths: options.paths };
60-
// The bundled runtime needs no selection; its bootstrap re-attests before binding.
61-
if (rushVersion === BUNDLED_RUSH_VERSION)
62-
return getDaemonConnectionOptions(repoRoot, rushVersion, environment, true);
63-
// Selecting another installation uses the engine's installer APIs, so it is loaded only when needed.
64-
const { selectDaemonLauncherAsync } = await import(
65-
'@rushstack/rush-daemon/lib/VersionSelectedDaemonLauncher'
66-
);
67-
const launch: IVersionSelectedDaemonLaunch = await selectDaemonLauncherAsync({
68-
repoRoot: fs.realpathSync.native(repoRoot),
69-
rushVersion,
70-
environment
71-
});
56+
if (rushVersion === getBundledRushVersion()) {
57+
const daemonPackagePath: string = require.resolve('@rushstack/rush-daemon/package.json');
58+
if (readDaemonInstallationMetadata(daemonPackagePath).rushVersion !== rushVersion) {
59+
// Preserve the eager synchronous launcher error.
60+
return getDaemonConnectionOptions(repoRoot, rushVersion, environment, true);
61+
}
62+
// The bundled runtime's bootstrap re-attests before binding. Resolve it only if a start is needed.
63+
return {
64+
...options,
65+
resolveStartCommandAsync: async () =>
66+
getDaemonConnectionOptions(repoRoot, rushVersion, environment, true).startCommand!
67+
};
68+
}
69+
const { selectDaemonLauncherAsync } = loadVersionSelectedDaemonLauncher();
70+
const launch: VersionSelectedDaemonLauncherModule.IVersionSelectedDaemonLaunch =
71+
await selectDaemonLauncherAsync({
72+
repoRoot: fs.realpathSync.native(repoRoot),
73+
rushVersion,
74+
environment
75+
});
7276
return { ...options, expectedDaemonVersion: launch.daemonVersion, startCommand: launch.startCommand };
7377
}

‎apps/rush-cli-client/src/launchClient.ts‎

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33

44
import * as path from 'node:path';
55

6+
import type { IDaemonConfigurationJson } from '@microsoft/rush-lib';
7+
// A deep import keeps the warm connect path from evaluating the @microsoft/rush-lib entry point.
68
import {
79
daemonEnvironmentVariables,
8-
resolveDaemonConfiguration,
9-
type IDaemonConfigurationJson
10+
resolveDaemonConfiguration
1011
} from '@microsoft/rush-lib/lib/api/DaemonConfiguration';
11-
import { tryFindRushJsonLocation } from '@microsoft/rush-lib/lib/api/RushJsonLocation';
1212
import { JsonFile } from '@rushstack/node-core-library';
1313
import {
1414
DaemonClientError,
@@ -21,15 +21,19 @@ import {
2121
} from '@rushstack/rush-client-core';
2222
import type { DaemonVerbosity, IDaemonRequestEnvelope } from '@rushstack/rush-daemon-protocol';
2323
import { ConsoleTerminalProvider } from '@rushstack/terminal';
24-
import { DaemonLauncherUnavailableError } from '@rushstack/rush-daemon/lib/DaemonLaunchCommand';
2524

26-
import { BUNDLED_RUSH_VERSION } from './bundledRushVersion';
2725
import { executeDaemonCommandAsync } from './daemonCommands';
2826
import { formatAdmissionFailure, getConfiguredAdmission } from './ClientAdmissionControls';
2927
import { ClientOperationRenderer } from './ClientOperationRenderer';
3028
import { getDaemonConnectionOptionsAsync } from './daemonConnectionOptions';
3129
import { selectClientRoute, type IClientRoute } from './routing';
3230
import { writeStreamAsync } from './writeStreamAsync';
31+
import {
32+
getBundledRushVersion,
33+
loadMinimalRushConfiguration,
34+
loadVersionSelectedDaemonLauncher,
35+
tryFindRushJsonLocation
36+
} from './lazyRushModules';
3337

3438
interface IWorkspaceJson {
3539
readonly rushVersion: string;
@@ -39,7 +43,7 @@ interface IWorkspaceJson {
3943
export async function launchClientAsync(rushx: boolean): Promise<void> {
4044
const cwd: string = process.cwd();
4145
const environment: Readonly<NodeJS.ProcessEnv> = Object.freeze({ ...process.env });
42-
const rushJsonPath: string | undefined = tryFindRushJsonLocation({ startingFolder: cwd });
46+
const rushJsonPath: string | undefined = tryFindRushJsonLocation(cwd);
4347
const workspace: IWorkspaceJson | undefined = rushJsonPath ? JsonFile.load(rushJsonPath) : undefined;
4448
const config: Readonly<Required<IDaemonConfigurationJson>> = resolveDaemonConfiguration(
4549
workspace?.daemon,
@@ -53,7 +57,7 @@ export async function launchClientAsync(rushx: boolean): Promise<void> {
5357
hasTerminal: !!(process.stdin.isTTY || process.stdout.isTTY || process.stderr.isTTY)
5458
});
5559
const selectedVersion: string =
56-
environment.RUSH_PREVIEW_VERSION ?? workspace?.rushVersion ?? BUNDLED_RUSH_VERSION;
60+
environment.RUSH_PREVIEW_VERSION ?? workspace?.rushVersion ?? getBundledRushVersion();
5761
if (!rushx && route.commandName === 'daemon') {
5862
if ((route.argv[1] === 'start' || route.argv[1] === 'restart') && process.argv.includes('--no-daemon')) {
5963
throw new Error(`--no-daemon cannot be combined with daemon ${route.argv[1]}.`);
@@ -120,7 +124,10 @@ export async function launchClientAsync(rushx: boolean): Promise<void> {
120124
};
121125
client = await connectOrStartDaemonAsync(connection);
122126
} catch (error) {
123-
if (!(error instanceof DaemonClientError) && !(error instanceof DaemonLauncherUnavailableError))
127+
if (
128+
!(error instanceof DaemonClientError) &&
129+
!(error instanceof loadVersionSelectedDaemonLauncher().DaemonLauncherUnavailableError)
130+
)
124131
throw error;
125132
process.stderr.write(`rush-client: ${error.message} Using in-process Rush.\n`);
126133
launchInProcess(route.argv, rushx, selectedVersion);
@@ -154,9 +161,9 @@ export async function launchClientAsync(rushx: boolean): Promise<void> {
154161
};
155162
try {
156163
if (rushx) {
157-
// Rushx discovery output needs the frontend's configuration loader, which loads the engine.
158-
const { MinimalRushConfiguration } = await import('@microsoft/rush/lib/MinimalRushConfiguration');
159-
MinimalRushConfiguration.loadFromDefaultLocation((line) => discoveryLines.push(line));
164+
loadMinimalRushConfiguration().MinimalRushConfiguration.loadFromDefaultLocation((line) =>
165+
discoveryLines.push(line)
166+
);
160167
}
161168
await renderer.initializeAsync();
162169
outcome = await executeWithDaemonRestartAsync(client, connection, {
@@ -219,7 +226,7 @@ function launchInProcess(argv: ReadonlyArray<string>, rushx: boolean, selectedVe
219226
const executable: string = rushx ? 'rushx' : 'rush';
220227
const rushFolder: string = path.dirname(require.resolve('@microsoft/rush/package.json'));
221228
process.argv = [process.execPath, path.join(rushFolder, 'bin', executable), ...argv];
222-
if (selectedVersion !== BUNDLED_RUSH_VERSION) {
229+
if (selectedVersion !== getBundledRushVersion()) {
223230
// Old Rush releases reject new RUSH_* names. Only strip this launcher's own inputs;
224231
// the request snapshot was captured earlier and is never mutated.
225232
for (const name of [...Object.values(daemonEnvironmentVariables), 'RUSH_DAEMON_EXPERIMENTAL']) {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// The warm daemon connect path must not load the @microsoft/rush-lib bundle (hundreds of milliseconds).
5+
// Modules that depend on it are loaded here on demand, only by fallback, rushx, startup and version selection.
6+
7+
import * as fs from 'node:fs';
8+
import * as path from 'node:path';
9+
10+
import type * as RushLibModule from '@microsoft/rush-lib';
11+
import type * as MinimalRushConfigurationModule from '@microsoft/rush/lib/MinimalRushConfiguration';
12+
import type * as VersionSelectedDaemonLauncherModule from '@rushstack/rush-daemon/lib/VersionSelectedDaemonLauncher';
13+
14+
export function loadRushLib(): typeof RushLibModule {
15+
return require('@microsoft/rush-lib');
16+
}
17+
18+
export function loadMinimalRushConfiguration(): typeof MinimalRushConfigurationModule {
19+
return require('@microsoft/rush/lib/MinimalRushConfiguration');
20+
}
21+
22+
export function loadVersionSelectedDaemonLauncher(): typeof VersionSelectedDaemonLauncherModule {
23+
return require('@rushstack/rush-daemon/lib/VersionSelectedDaemonLauncher');
24+
}
25+
26+
/** Equals `Rush.version`. */
27+
export function getBundledRushVersion(): string {
28+
return (require('@microsoft/rush-lib/package.json') as { version: string }).version;
29+
}
30+
31+
/** Same search as `RushConfiguration.tryFindRushJsonLocation({ startingFolder })`. */
32+
export function tryFindRushJsonLocation(startingFolder: string): string | undefined {
33+
let currentFolder: string = startingFolder;
34+
let parentFolder: string = path.dirname(currentFolder);
35+
while (parentFolder && parentFolder !== currentFolder) {
36+
const rushJsonFilename: string = path.join(currentFolder, 'rush.json');
37+
if (fs.existsSync(rushJsonFilename)) return rushJsonFilename;
38+
currentFolder = parentFolder;
39+
parentFolder = path.dirname(currentFolder);
40+
}
41+
return undefined;
42+
}

‎apps/rush-cli-client/src/routing.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
// See LICENSE in the project root for license information.
33

44
import type { IDaemonRequestAdmissionOptions } from '@rushstack/rush-daemon-protocol';
5-
import {
6-
parseRushXCommandLineArguments,
7-
type IRushXCommandLineArguments
8-
} from '@microsoft/rush-lib/lib/cli/RushXCommandLineArguments';
5+
import type { IRushXCommandLineArguments } from '@microsoft/rush-lib';
96

7+
import { loadRushLib } from './lazyRushModules';
108
import { parseClientAdmissionControls, type IClientAdmissionControls } from './ClientAdmissionControls';
119

1210
const neverDaemonize: ReadonlySet<string> = new Set([
@@ -56,7 +54,7 @@ export function selectClientRoute(options: IClientRouteOptions): IClientRoute {
5654
...(separator < 0 ? [] : controls.argv.slice(separator))
5755
];
5856
const rushxArguments: IRushXCommandLineArguments | undefined = options.rushx
59-
? parseRushXCommandLineArguments(argv, options.environment)
57+
? loadRushLib().RushXCommand.parseArguments(argv, options.environment)
6058
: undefined;
6159
const commandName: string | undefined = rushxArguments ? rushxArguments.commandName || undefined : argv[0];
6260
const reporterControls: boolean =

‎apps/rush-cli-client/src/test/ClientAdmissionControls.test.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ describe(getConfiguredAdmission.name, () => {
6969
});
7070

7171
it('keeps an explicitly configured timeout as one absolute deadline', () => {
72-
expect(getConfiguredAdmission({ queueTimeoutSeconds: 1.5, explicit: true })).toEqual({
73-
waitTimeoutMs: 1500
74-
});
72+
expect(getConfiguredAdmission({ queueTimeoutSeconds: 1.5, explicit: true })).toEqual({ waitTimeoutMs: 1500 });
7573
});
7674
});
7775

‎apps/rush-cli-client/src/test/NativeBuildTestFixture.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export interface INativeBuildTestFixture {
2727
readonly folder: string;
2828
readonly environment: NodeJS.ProcessEnv;
2929
readonly paths: IDaemonPaths;
30-
invokeAsync(argv: ReadonlyArray<string>, rushx?: boolean): Promise<INativeBuildResult>;
30+
invokeAsync(
31+
argv: ReadonlyArray<string>,
32+
rushx?: boolean,
33+
nodeArgs?: ReadonlyArray<string>
34+
): Promise<INativeBuildResult>;
3135
snapshotAsync(...args: string[]): Promise<IDaemonGraphSnapshotPayload['snapshot']>;
3236
runAsync(work: (fixture: INativeBuildTestFixture) => Promise<void>): Promise<void>;
3337
trackWatch(child: ChildProcess, closed: Promise<unknown[]>): void;
@@ -125,17 +129,22 @@ export function createNativeBuildTestFixture(): INativeBuildTestFixture {
125129
})
126130
);
127131

128-
function invokeAsync(argv: ReadonlyArray<string>, rushx: boolean = false): Promise<INativeBuildResult> {
132+
function invokeAsync(
133+
argv: ReadonlyArray<string>,
134+
rushx: boolean = false,
135+
nodeArgs: ReadonlyArray<string> = []
136+
): Promise<INativeBuildResult> {
129137
if (!acceptingInvocations) throw new Error('The native build fixture is already closing.');
130-
return spawnClientAsync(argv, rushx);
138+
return spawnClientAsync(argv, rushx, nodeArgs);
131139
}
132140

133141
async function spawnClientAsync(
134142
argv: ReadonlyArray<string>,
135-
rushx: boolean = false
143+
rushx: boolean = false,
144+
nodeArgs: ReadonlyArray<string> = []
136145
): Promise<INativeBuildResult> {
137146
const entry: string = path.resolve(__dirname, rushx ? '../../bin/rushx-client' : '../../bin/rush-client');
138-
const child = spawn(process.execPath, [entry, ...argv], {
147+
const child = spawn(process.execPath, [...nodeArgs, entry, ...argv], {
139148
cwd: rushx ? path.join(folder, 'b') : folder,
140149
env: environment,
141150
stdio: ['ignore', 'pipe', 'pipe']
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// Preloaded with `node --require` by startupBudget.test.ts. Reports the loaded module count and the loaded
5+
// Rush modules on one short stderr line (a large synchronous write to a pipe at exit can be truncated).
6+
import * as fs from 'node:fs';
7+
8+
process.once('exit', () => {
9+
const modules: string[] = Object.keys(require.cache);
10+
const report: { count: number; rushModules: string[] } = {
11+
count: modules.length,
12+
rushModules: modules.filter((name) => /[\\/](rush-lib|rush|rush-daemon)[\\/](lib-commonjs|dist)[\\/]/.test(name))
13+
};
14+
fs.writeSync(2, `\nrush-client-startup-modules:${JSON.stringify(report)}\n`);
15+
});

0 commit comments

Comments
 (0)