From 3e63a28ef7e3b2ddd62f480e17405abce345f938 Mon Sep 17 00:00:00 2001 From: b84330808 Date: Wed, 21 Oct 2026 13:17:49 +0900 Subject: [PATCH] [rush-serve-plugin] Forward the auto-picked port to the port parameter When no port is given, the serve plugin listens on an OS-assigned port and then writes that port back into the port parameter so that operations receive it. It did this by assigning `_value`, but ts-command-line now keeps parameter values in a native private field, so the assignment had no effect and the parameter stayed undefined. Set the value through `_setValue()` instead. Fixes #6144 Co-Authored-By: Claude Opus 5.5 --- ...serve-plugin-port_2026-10-21-04-18-12.json | 11 ++ .../src/phasedCommandHandler.ts | 3 +- .../src/test/phasedCommandHandler.test.ts | 115 ++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 common/changes/@microsoft/rush/fix-rush-serve-plugin-port_2026-10-21-04-18-12.json create mode 100644 rush-plugins/rush-serve-plugin/src/test/phasedCommandHandler.test.ts diff --git a/common/changes/@microsoft/rush/fix-rush-serve-plugin-port_2026-10-21-04-18-12.json b/common/changes/@microsoft/rush/fix-rush-serve-plugin-port_2026-10-21-04-18-12.json new file mode 100644 index 00000000000..4fd9d12987c --- /dev/null +++ b/common/changes/@microsoft/rush/fix-rush-serve-plugin-port_2026-10-21-04-18-12.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Fix an issue where `@rushstack/rush-serve-plugin` did not forward the port it picked automatically to the parameter specified by `portParameterLongName`, so operations that receive that parameter did not get the port.", + "type": "none", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "b84330808@gmail.com" +} \ No newline at end of file diff --git a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts index a135c7aec04..5ab0542ac4f 100644 --- a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts +++ b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts @@ -238,7 +238,8 @@ export async function phasedCommandHandler(options: IPhasedCommandHandlerOptions if (portParameter) { // Hack the value of the parsed command line parameter to the actual runtime value. // This will cause the resolved value to be forwarded to operations that may use it. - (portParameter as unknown as { _value?: string })._value = `${activePort}`; + // The value is kept in a private field, so it must be set through the internal `_setValue()` method. + (portParameter as unknown as { _setValue(data: unknown): void })._setValue(activePort); } logHost(); diff --git a/rush-plugins/rush-serve-plugin/src/test/phasedCommandHandler.test.ts b/rush-plugins/rush-serve-plugin/src/test/phasedCommandHandler.test.ts new file mode 100644 index 00000000000..387f50f0181 --- /dev/null +++ b/rush-plugins/rush-serve-plugin/src/test/phasedCommandHandler.test.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +jest.mock('@rushstack/debug-certificate-manager', () => { + return { + CertificateManager: class { + public async ensureCertificateAsync(): Promise<{}> { + // The server does not need a real certificate to start listening. + return {}; + } + } + }; +}); + +import type { + ICreateOperationsContext, + IPhasedCommand, + Operation, + RushConfiguration, + RushSession +} from '@rushstack/rush-sdk'; +import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; +import { + type CommandLineIntegerParameter, + DynamicCommandLineAction, + DynamicCommandLineParser +} from '@rushstack/ts-command-line'; + +import { phasedCommandHandler } from '../phasedCommandHandler'; + +type CreateOperationsAsync = ( + operations: Set, + context: ICreateOperationsContext +) => Promise>; + +describe(phasedCommandHandler.name, () => { + it('writes the port that the server listens on back to the port parameter', async () => { + const parser: DynamicCommandLineParser = new DynamicCommandLineParser({ + toolFilename: 'rush', + toolDescription: 'test' + }); + const action: DynamicCommandLineAction = new DynamicCommandLineAction({ + actionName: 'start', + summary: 'test', + documentation: 'test' + }); + parser.addAction(action); + const portParameter: CommandLineIntegerParameter = action.defineIntegerParameter({ + parameterLongName: '--port', + argumentName: 'PORT', + description: 'The port to serve on' + }); + // The port is not specified, so the server should pick one + await parser.executeWithoutErrorHandlingAsync(['start']); + expect(portParameter.value).toBeUndefined(); + + let createOperationsAsync: CreateOperationsAsync | undefined; + const sessionAbortController: AbortController = new AbortController(); + const command: IPhasedCommand = { + actionName: 'start', + hooks: { + createOperationsAsync: { + tapPromise: (options: unknown, fn: CreateOperationsAsync) => { + createOperationsAsync = fn; + } + }, + onGraphCreatedAsync: { + tap: () => { + // Not needed by this test + } + } + }, + sessionAbortController + } as unknown as IPhasedCommand; + + const terminalProvider: StringBufferTerminalProvider = new StringBufferTerminalProvider(); + const terminal: Terminal = new Terminal(terminalProvider); + const rushSession: RushSession = { + getLogger: () => ({ terminal }) + } as unknown as RushSession; + + await phasedCommandHandler({ + rushSession, + rushConfiguration: {} as RushConfiguration, + command, + portParameterLongName: '--port', + logServePath: undefined, + globalRoutingRules: [], + buildStatusWebSocketPath: undefined + }); + + const context: ICreateOperationsContext = { + isWatch: true, + generateFullGraph: false, + projectSelection: new Set(), + customParameters: new Map([['--port', portParameter]]) + } as unknown as ICreateOperationsContext; + + try { + await createOperationsAsync!(new Set(), context); + + const port: number | undefined = portParameter.value; + expect(typeof port).toBe('number'); + expect(port).toBeGreaterThan(0); + expect(terminalProvider.getOutput()).toContain(`https://localhost:${port}/`); + + // This is how the value is forwarded to the operations that receive the parameter + const argList: string[] = []; + portParameter.appendToArgList(argList); + expect(argList).toEqual(['--port', `${port}`]); + } finally { + sessionAbortController.abort(); + } + }); +});