From 8ea2d763d2260c2412d53a606f3d684dc4f52e9d Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:25:24 -0700 Subject: [PATCH 1/4] refactor: use native Node.js APIs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8ebd5bf2-c44b-42d5-be25-e7936d4b0a14 --- .../src/utils/PackageUpdateChecker.ts | 6 +--- .../utils/test/PackageUpdateChecker.test.ts | 31 +++++++++++++++++++ ...ive-standard-apis_2026-08-18-03-14-05.json | 11 +++++++ ...ive-standard-apis_2026-08-18-03-14-05.json | 11 +++++++ libraries/node-core-library/src/Text.ts | 15 ++++++--- .../node-core-library/src/test/Text.test.ts | 11 +++++++ 6 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json create mode 100644 common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json diff --git a/apps/lockfile-explorer/src/utils/PackageUpdateChecker.ts b/apps/lockfile-explorer/src/utils/PackageUpdateChecker.ts index 9692d4bf43a..cf353b347d2 100644 --- a/apps/lockfile-explorer/src/utils/PackageUpdateChecker.ts +++ b/apps/lockfile-explorer/src/utils/PackageUpdateChecker.ts @@ -88,10 +88,8 @@ const CACHE_FOLDER: string = `${homedir()}/.rushstack/update-checks`; async function _tryFetchLatestVersionAsync(packageName: string): Promise { const url: string = `${REGISTRY_BASE_URL}/${encodeURIComponent(packageName)}/latest`; - const controller: AbortController = new AbortController(); - const timeout: NodeJS.Timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); try { - const response: Response = await fetch(url, { signal: controller.signal }); + const response: Response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); if (!response.ok) { return undefined; } @@ -101,8 +99,6 @@ async function _tryFetchLatestVersionAsync(packageName: string): Promise { expect(saveSpy).not.toHaveBeenCalled(); }); + it('returns undefined when the registry request times out', async () => { + loadSpy.mockRejectedValue(new Error('ENOENT')); + + const nativeTimeout: typeof AbortSignal.timeout = AbortSignal.timeout.bind(AbortSignal); + const timeoutSpy: jest.SpyInstance = jest + .spyOn(AbortSignal, 'timeout') + .mockImplementation(() => nativeTimeout(1)); + + let fetchSignal: AbortSignal | undefined; + fetchSpy.mockImplementation( + async (input: Parameters[0], init?: RequestInit): Promise => { + void input; + fetchSignal = init?.signal as AbortSignal; + await new Promise((resolve) => { + fetchSignal!.addEventListener('abort', () => resolve(), { once: true }); + }); + throw fetchSignal.reason; + } + ); + + const checker: PackageUpdateChecker = new PackageUpdateChecker({ + packageName: PACKAGE_NAME, + currentVersion: CURRENT_VERSION + }); + expect(await checker.tryGetUpdateAsync()).toBeUndefined(); + expect(fetchSignal?.aborted).toBe(true); + expect((fetchSignal?.reason as DOMException).name).toBe('TimeoutError'); + expect(timeoutSpy).toHaveBeenCalledWith(5000); + expect(saveSpy).not.toHaveBeenCalled(); + }); + it('returns undefined on non-ok HTTP response', async () => { loadSpy.mockRejectedValue(new Error('ENOENT')); fetchSpy.mockResolvedValue(makeFetchResponse('', false)); diff --git a/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json b/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json new file mode 100644 index 00000000000..cc3e3649f8f --- /dev/null +++ b/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@rushstack/lockfile-explorer", + "comment": "Use AbortSignal.timeout() for npm registry request timeouts.", + "type": "patch" + } + ], + "packageName": "@rushstack/lockfile-explorer", + "email": "5100938+bmiddha@users.noreply.github.com" +} diff --git a/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json b/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json new file mode 100644 index 00000000000..fbfd5042a6b --- /dev/null +++ b/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Delegate Text.replaceAll() to the native implementation, including native empty search string behavior.", + "type": "patch" + } + ], + "packageName": "@rushstack/node-core-library", + "email": "5100938+bmiddha@users.noreply.github.com" +} diff --git a/libraries/node-core-library/src/Text.ts b/libraries/node-core-library/src/Text.ts index 465c240c5c0..a74e96cf99a 100644 --- a/libraries/node-core-library/src/Text.ts +++ b/libraries/node-core-library/src/Text.ts @@ -97,15 +97,20 @@ function* readLinesFromChunk( */ export class Text { /** - * Returns the same thing as targetString.replace(searchValue, replaceValue), except that - * all matches are replaced, rather than just the first match. + * Replaces every occurrence of `searchValue` with `replaceValue`. + * + * @remarks + * This method has the same behavior as {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll | String.prototype.replaceAll}. + * In particular, if `searchValue` is an empty string, `replaceValue` is inserted before the first + * UTF-16 code unit, between each code unit, and after the last code unit. + * * @param input - The string to be modified * @param searchValue - The value to search for * @param replaceValue - The replacement text */ - public static replaceAll(input: string, searchValue: string, replaceValue: string): string { - return input.split(searchValue).join(replaceValue); - } + public static replaceAll(input: string, searchValue: string, replaceValue: string): string { + return input.replaceAll(searchValue, replaceValue); + } /** * Converts all newlines in the provided string to use Windows-style CRLF end of line characters. diff --git a/libraries/node-core-library/src/test/Text.test.ts b/libraries/node-core-library/src/test/Text.test.ts index a4567b25f98..82f16e4aa78 100644 --- a/libraries/node-core-library/src/test/Text.test.ts +++ b/libraries/node-core-library/src/test/Text.test.ts @@ -4,6 +4,17 @@ import { Text } from '../Text'; describe(Text.name, () => { + describe(Text.replaceAll.name, () => { + it('replaces every occurrence', () => { + expect(Text.replaceAll('one two one', 'one', 'three')).toEqual('three two three'); + }); + + it('uses native empty search string semantics', () => { + expect(Text.replaceAll('abc', '', '-')).toEqual('-a-b-c-'); + expect(Text.replaceAll('', '', '-')).toEqual('-'); + }); + }); + describe(Text.padEnd.name, () => { it("Throws an exception if the padding character isn't a single character", () => { expect(() => Text.padEnd('123', 1, '')).toThrow(); From 1223d1917582682abd06c6c5e3fae4dcaa1aeac4 Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:47:14 -0700 Subject: [PATCH 2/4] refactor: deprecate Text.replaceAll Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8ebd5bf2-c44b-42d5-be25-e7936d4b0a14 --- .../src/analyzer/SourceFileLocationFormatter.ts | 4 ++-- apps/api-extractor/src/api/ExtractorConfig.ts | 5 ++--- .../src/graph/lfxGraphLoader.ts | 16 +++++++--------- apps/rundown/src/Rundown.ts | 4 ++-- ...native-standard-apis_2026-08-18-03-14-05.json | 2 +- common/reviews/api/node-core-library.api.md | 1 + libraries/node-core-library/src/JsonFile.ts | 2 +- libraries/node-core-library/src/Text.ts | 8 +++++--- .../src/test/Executable.test.ts | 3 +-- .../node-core-library/src/test/Text.test.ts | 11 ----------- .../src/api/test/RushConfiguration.test.ts | 4 ++-- .../src/cli/scriptActions/GlobalScriptAction.ts | 3 +-- libraries/rush-lib/src/logic/PublishUtilities.ts | 3 +-- .../logic/installManager/RushInstallManager.ts | 9 ++------- .../src/logic/operations/ShellOperationRunner.ts | 3 +-- .../rush-lib/src/utilities/AsyncRecycler.ts | 4 ++-- .../src/test/TextRewriterTransform.test.ts | 4 ++-- 17 files changed, 33 insertions(+), 53 deletions(-) diff --git a/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts b/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts index 939f9aa5ba2..f9b6d593244 100644 --- a/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts +++ b/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts @@ -5,7 +5,7 @@ import * as path from 'node:path'; import type * as ts from 'typescript'; -import { Path, Text } from '@rushstack/node-core-library'; +import { Path } from '@rushstack/node-core-library'; export interface ISourceFileLocationFormatOptions { sourceFileLine?: number; @@ -47,7 +47,7 @@ export class SourceFileLocationFormatter { } // Convert it to a Unix-style path - scrubbedPath = Text.replaceAll(scrubbedPath, '\\', '/'); + scrubbedPath = scrubbedPath.replaceAll('\\', '/'); result += scrubbedPath; if (options.sourceFileLine) { diff --git a/apps/api-extractor/src/api/ExtractorConfig.ts b/apps/api-extractor/src/api/ExtractorConfig.ts index f3e33f46825..50ccb9b48a1 100644 --- a/apps/api-extractor/src/api/ExtractorConfig.ts +++ b/apps/api-extractor/src/api/ExtractorConfig.ts @@ -17,7 +17,6 @@ import { PackageJsonLookup, type INodePackageJson, PackageName, - Text, InternalError, Path, NewlineKind @@ -1310,8 +1309,8 @@ function _expandStringWithTokens( ): string { value = value ? value.trim() : ''; if (value !== '') { - value = Text.replaceAll(value, '', tokenContext.unscopedPackageName); - value = Text.replaceAll(value, '', tokenContext.packageName); + value = value.replaceAll('', tokenContext.unscopedPackageName); + value = value.replaceAll('', tokenContext.packageName); const projectFolderToken: string = ''; if (value.indexOf(projectFolderToken) === 0) { diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index 732548d9743..305c7c30363 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -4,8 +4,6 @@ import type * as lockfileTypes from '@pnpm/lockfile.types'; import type * as pnpmTypes from '@pnpm/types'; -import { Text } from '@rushstack/node-core-library'; - import { type ILfxGraphDependencyOptions, type ILfxGraphEntryOptions, @@ -398,9 +396,9 @@ function createPackageLockfileEntry(options: { // Rewrite to: // "@rushstack/m@1.0.0; @rushstack/n@2.0.0" - suffix = Text.replaceAll(suffix, ')(', '; '); - suffix = Text.replaceAll(suffix, '(', ''); - suffix = Text.replaceAll(suffix, ')', ''); + suffix = suffix.replaceAll(')(', '; '); + suffix = suffix.replaceAll('(', ''); + suffix = suffix.replaceAll(')', ''); result.entrySuffix = suffix; // @rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) @@ -412,10 +410,10 @@ function createPackageLockfileEntry(options: { // --> @rushstack+l@1.0.0_@rushstack+m@1.0.0_@rushstack+n@2.0.0 // @rushstack/l 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - dotPnpmSubfolder = Text.replaceAll(slashlessRawEntryId, '/', '+'); - dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, ')(', '_'); - dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, '(', '_'); - dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, ')', ''); + dotPnpmSubfolder = slashlessRawEntryId.replaceAll('/', '+'); + dotPnpmSubfolder = dotPnpmSubfolder.replaceAll(')(', '_'); + dotPnpmSubfolder = dotPnpmSubfolder.replaceAll('(', '_'); + dotPnpmSubfolder = dotPnpmSubfolder.replaceAll(')', ''); } // Example: diff --git a/apps/rundown/src/Rundown.ts b/apps/rundown/src/Rundown.ts index 9b63ac8438f..28986d02238 100644 --- a/apps/rundown/src/Rundown.ts +++ b/apps/rundown/src/Rundown.ts @@ -6,7 +6,7 @@ import * as path from 'node:path'; import stringArgv from 'string-argv'; -import { FileSystem, PackageJsonLookup, Sort, Text } from '@rushstack/node-core-library'; +import { FileSystem, PackageJsonLookup, Sort } from '@rushstack/node-core-library'; import type { IpcMessage } from './LauncherTypes'; @@ -57,7 +57,7 @@ export class Rundown { importedPackageFolders.add(path.basename(importedPackageFolder)); } else { const relativePath: string = path.relative(process.cwd(), importedPackageFolder); - importedPackageFolders.add(Text.replaceAll(relativePath, '\\', '/')); + importedPackageFolders.add(relativePath.replaceAll('\\', '/')); } } else { // If the importedPath does not belong to an NPM package, then rundown-snapshot.log can ignore it. diff --git a/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json b/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json index fbfd5042a6b..7f3833b7951 100644 --- a/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json +++ b/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@rushstack/node-core-library", - "comment": "Delegate Text.replaceAll() to the native implementation, including native empty search string behavior.", + "comment": "Deprecate Text.replaceAll() in favor of the native String.prototype.replaceAll() method.", "type": "patch" } ], diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index bb10f9b3d72..d22e4840198 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -968,6 +968,7 @@ export class Text { static padStart(s: string, minimumLength: number, paddingCharacter?: string): string; static readLinesFromIterable(iterable: Iterable, options?: IReadLinesFromIterableOptions): Generator; static readLinesFromIterableAsync(iterable: AsyncIterable, options?: IReadLinesFromIterableOptions): AsyncGenerator; + // @deprecated static replaceAll(input: string, searchValue: string, replaceValue: string): string; static reverse(s: string): string; static splitByNewLines(s: undefined): undefined; diff --git a/libraries/node-core-library/src/JsonFile.ts b/libraries/node-core-library/src/JsonFile.ts index 151edd6d366..5eb144225bb 100644 --- a/libraries/node-core-library/src/JsonFile.ts +++ b/libraries/node-core-library/src/JsonFile.ts @@ -580,7 +580,7 @@ function _formatJsonHeaderComment(headerComment: string): string { JSON.stringify(line) ); } - result.push(Text.replaceAll(line, '\r', '')); + result.push(line.replaceAll('\r', '')); } return lines.join('\n') + '\n'; } diff --git a/libraries/node-core-library/src/Text.ts b/libraries/node-core-library/src/Text.ts index a74e96cf99a..23fbf483881 100644 --- a/libraries/node-core-library/src/Text.ts +++ b/libraries/node-core-library/src/Text.ts @@ -107,10 +107,12 @@ export class Text { * @param input - The string to be modified * @param searchValue - The value to search for * @param replaceValue - The replacement text + * + * @deprecated Use `String.prototype.replaceAll()` instead. */ - public static replaceAll(input: string, searchValue: string, replaceValue: string): string { - return input.replaceAll(searchValue, replaceValue); - } + public static replaceAll(input: string, searchValue: string, replaceValue: string): string { + return input.replaceAll(searchValue, replaceValue); + } /** * Converts all newlines in the provided string to use Windows-style CRLF end of line characters. diff --git a/libraries/node-core-library/src/test/Executable.test.ts b/libraries/node-core-library/src/test/Executable.test.ts index df8e3d9003f..d3e3c7187d7 100644 --- a/libraries/node-core-library/src/test/Executable.test.ts +++ b/libraries/node-core-library/src/test/Executable.test.ts @@ -17,7 +17,6 @@ import { } from '../Executable'; import { FileSystem } from '../FileSystem'; import { PosixModeBits } from '../PosixModeBits'; -import { Text } from '../Text'; import { Readable } from 'node:stream'; describe('Executable process tests', () => { @@ -101,7 +100,7 @@ describe('Executable process tests', () => { test('Executable.tryResolve() pathless', () => { const resolved: string | undefined = Executable.tryResolve('npm-binary-wrapper', options); expect(resolved).toBeDefined(); - const resolvedRelative: string = Text.replaceAll(path.relative(executableFolder, resolved!), '\\', '/'); + const resolvedRelative: string = path.relative(executableFolder, resolved!).replaceAll('\\', '/'); if (os.platform() === 'win32') { // On Windows, we should find npm-binary-wrapper.cmd instead of npm-binary-wrapper diff --git a/libraries/node-core-library/src/test/Text.test.ts b/libraries/node-core-library/src/test/Text.test.ts index 82f16e4aa78..a4567b25f98 100644 --- a/libraries/node-core-library/src/test/Text.test.ts +++ b/libraries/node-core-library/src/test/Text.test.ts @@ -4,17 +4,6 @@ import { Text } from '../Text'; describe(Text.name, () => { - describe(Text.replaceAll.name, () => { - it('replaces every occurrence', () => { - expect(Text.replaceAll('one two one', 'one', 'three')).toEqual('three two three'); - }); - - it('uses native empty search string semantics', () => { - expect(Text.replaceAll('abc', '', '-')).toEqual('-a-b-c-'); - expect(Text.replaceAll('', '', '-')).toEqual('-'); - }); - }); - describe(Text.padEnd.name, () => { it("Throws an exception if the padding character isn't a single character", () => { expect(() => Text.padEnd('123', 1, '')).toThrow(); diff --git a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts index 039dbb7df31..3a5a240e91c 100644 --- a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts @@ -3,7 +3,7 @@ import * as path from 'node:path'; -import { JsonFile, Path, Text } from '@rushstack/node-core-library'; +import { JsonFile, Path } from '@rushstack/node-core-library'; import { RushConfiguration } from '../RushConfiguration'; import type { ApprovedPackagesPolicy } from '../ApprovedPackagesPolicy'; import { RushConfigurationProject } from '../RushConfigurationProject'; @@ -11,7 +11,7 @@ import { EnvironmentConfiguration } from '../EnvironmentConfiguration'; import { DependencyType } from '../PackageJsonEditor'; function normalizePathForComparison(pathToNormalize: string): string { - return Text.replaceAll(pathToNormalize, '\\', '/').toUpperCase(); + return pathToNormalize.replaceAll('\\', '/').toUpperCase(); } function assertPathProperty(validatedPropertyName: string, absolutePath: string, relativePath: string): void { diff --git a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts index 0f0c38923e7..231508a66c7 100644 --- a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts @@ -11,7 +11,6 @@ import { type IPackageJson, JsonFile, AlreadyReportedError, - Text } from '@rushstack/node-core-library'; import { Colorize } from '@rushstack/terminal'; @@ -264,7 +263,7 @@ export class GlobalScriptAction extends BaseScriptAction { ): string { let expandedShellCommand: string = shellCommand; for (const [token, tokenReplacement] of Object.entries(tokenContext)) { - expandedShellCommand = Text.replaceAll(expandedShellCommand, `<${token}>`, tokenReplacement); + expandedShellCommand = expandedShellCommand.replaceAll(`<${token}>`, tokenReplacement); } return expandedShellCommand; } diff --git a/libraries/rush-lib/src/logic/PublishUtilities.ts b/libraries/rush-lib/src/logic/PublishUtilities.ts index 1005e20922b..a601b033a3d 100644 --- a/libraries/rush-lib/src/logic/PublishUtilities.ts +++ b/libraries/rush-lib/src/logic/PublishUtilities.ts @@ -15,7 +15,6 @@ import { type IPackageJson, JsonFile, FileConstants, - Text, Enum, InternalError, Executable @@ -282,7 +281,7 @@ export class PublishUtilities { if (secretSubstring && secretSubstring.length > 0) { // Avoid printing the NPM publish token on the console when displaying the commandArgs - commandArgs = Text.replaceAll(commandArgs, secretSubstring, '<>'); + commandArgs = commandArgs.replaceAll(secretSubstring, '<>'); } // eslint-disable-next-line no-console diff --git a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts index 4e1c294ba86..cf9e51be470 100644 --- a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts @@ -9,7 +9,6 @@ import * as ssri from 'ssri'; import { JsonFile, - Text, FileSystem, FileConstants, Sort, @@ -558,11 +557,7 @@ export class RushInstallManager extends BaseInstallManager { // eslint-disable-next-line no-console console.log(`Deleting ${pathToDeleteWithoutStar}\\*`); // Glob can't handle Windows paths - const normalizedPathToDeleteWithoutStar: string = Text.replaceAll( - pathToDeleteWithoutStar, - '\\', - '/' - ); + const normalizedPathToDeleteWithoutStar: string = pathToDeleteWithoutStar.replaceAll('\\', '/'); const { default: glob } = await import('fast-glob'); const tempModulePaths: string[] = await glob( @@ -693,7 +688,7 @@ export class RushInstallManager extends BaseInstallManager { RushConstants.rushTempNpmScope ); // Glob can't handle Windows paths - const normalizedPathToDeleteWithoutStar: string = Text.replaceAll(pathToDeleteWithoutStar, '\\', '/'); + const normalizedPathToDeleteWithoutStar: string = pathToDeleteWithoutStar.replaceAll('\\', '/'); let anyChanges: boolean = false; diff --git a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts index c4b368ed38a..df76928fe70 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts @@ -3,7 +3,6 @@ import type * as child_process from 'node:child_process'; -import { Text } from '@rushstack/node-core-library'; import { type ITerminal, type ITerminalProvider, TerminalProviderSeverity } from '@rushstack/terminal'; import type { IPhase } from '../../api/CommandLineConfiguration'; @@ -200,7 +199,7 @@ export function convertSlashesForWindows(command: string): string { // "bin\blarg --path ./config/blah.json && a/b" // // NOTE: we don't attempt to process the path parameter or stuff after "&&" - return Text.replaceAll(commandPart, '/', '\\') + remainder; + return commandPart.replaceAll('/', '\\') + remainder; } } diff --git a/libraries/rush-lib/src/utilities/AsyncRecycler.ts b/libraries/rush-lib/src/utilities/AsyncRecycler.ts index 98ec9995096..eb63ca734b1 100644 --- a/libraries/rush-lib/src/utilities/AsyncRecycler.ts +++ b/libraries/rush-lib/src/utilities/AsyncRecycler.ts @@ -5,7 +5,7 @@ import * as child_process from 'node:child_process'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { Text, Path, FileSystem, type FolderItem } from '@rushstack/node-core-library'; +import { Path, FileSystem, type FolderItem } from '@rushstack/node-core-library'; import { Utilities } from './Utilities'; import { IS_WINDOWS } from './executionUtilities'; @@ -131,7 +131,7 @@ export class AsyncRecycler { command = 'cmd.exe'; // In PowerShell single-quote literals, single quotes are escaped by doubling them - const escapedRecyclerFolder: string = Text.replaceAll(this.recyclerFolder, "'", "''"); + const escapedRecyclerFolder: string = this.recyclerFolder.replaceAll("'", "''"); // As of PowerShell 3.0, the "\\?" prefix can be used for paths that exceed MAX_PATH. // (This prefix does not seem to work for cmd.exe's "rd" command.) diff --git a/libraries/terminal/src/test/TextRewriterTransform.test.ts b/libraries/terminal/src/test/TextRewriterTransform.test.ts index 583cf70316d..a1204c9e759 100644 --- a/libraries/terminal/src/test/TextRewriterTransform.test.ts +++ b/libraries/terminal/src/test/TextRewriterTransform.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { NewlineKind, Text } from '@rushstack/node-core-library'; +import { NewlineKind } from '@rushstack/node-core-library'; import { Colorize } from '../Colorize'; import { TerminalChunkKind } from '../ITerminalChunk'; @@ -32,7 +32,7 @@ describe(TextRewriterTransform.name, () => { expect( mockWritable.chunks.map((x) => ({ kind: x.kind, - text: Text.replaceAll(x.text, '\n', '[n]') + text: x.text.replaceAll('\n', '[n]') })) ).toMatchSnapshot(); }); From 4c88fa83e1ca32123c92ead1cc5f07320f354af1 Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:44:37 -0700 Subject: [PATCH 3/4] refactor: reuse path and newline helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8ebd5bf2-c44b-42d5-be25-e7936d4b0a14 --- .../src/analyzer/SourceFileLocationFormatter.ts | 2 +- apps/rundown/src/Rundown.ts | 4 ++-- .../native-standard-apis_2026-08-18-03-14-05.json | 2 +- .../native-standard-apis_2026-08-18-03-14-05.json | 4 ++-- libraries/node-core-library/src/JsonFile.ts | 4 +--- libraries/node-core-library/src/test/Executable.test.ts | 5 +++-- libraries/rush-lib/src/api/test/RushConfiguration.test.ts | 2 +- .../src/logic/installManager/RushInstallManager.ts | 8 +++++--- .../rush-lib/src/logic/operations/ShellOperationRunner.ts | 3 ++- 9 files changed, 18 insertions(+), 16 deletions(-) diff --git a/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts b/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts index f9b6d593244..ad01475144c 100644 --- a/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts +++ b/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts @@ -47,7 +47,7 @@ export class SourceFileLocationFormatter { } // Convert it to a Unix-style path - scrubbedPath = scrubbedPath.replaceAll('\\', '/'); + scrubbedPath = Path.convertToSlashes(scrubbedPath); result += scrubbedPath; if (options.sourceFileLine) { diff --git a/apps/rundown/src/Rundown.ts b/apps/rundown/src/Rundown.ts index 28986d02238..9a4dd720817 100644 --- a/apps/rundown/src/Rundown.ts +++ b/apps/rundown/src/Rundown.ts @@ -6,7 +6,7 @@ import * as path from 'node:path'; import stringArgv from 'string-argv'; -import { FileSystem, PackageJsonLookup, Sort } from '@rushstack/node-core-library'; +import { FileSystem, PackageJsonLookup, Path, Sort } from '@rushstack/node-core-library'; import type { IpcMessage } from './LauncherTypes'; @@ -57,7 +57,7 @@ export class Rundown { importedPackageFolders.add(path.basename(importedPackageFolder)); } else { const relativePath: string = path.relative(process.cwd(), importedPackageFolder); - importedPackageFolders.add(relativePath.replaceAll('\\', '/')); + importedPackageFolders.add(Path.convertToSlashes(relativePath)); } } else { // If the importedPath does not belong to an NPM package, then rundown-snapshot.log can ignore it. diff --git a/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json b/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json index cc3e3649f8f..22fad0f037d 100644 --- a/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json +++ b/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@rushstack/lockfile-explorer", - "comment": "Use AbortSignal.timeout() for npm registry request timeouts.", + "comment": "Use `AbortSignal.timeout()` for npm registry request timeouts.", "type": "patch" } ], diff --git a/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json b/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json index 7f3833b7951..8bef7a52cb5 100644 --- a/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json +++ b/common/changes/@rushstack/node-core-library/native-standard-apis_2026-08-18-03-14-05.json @@ -2,8 +2,8 @@ "changes": [ { "packageName": "@rushstack/node-core-library", - "comment": "Deprecate Text.replaceAll() in favor of the native String.prototype.replaceAll() method.", - "type": "patch" + "comment": "Deprecate `Text.replaceAll()` in favor of the native `String.prototype.replaceAll()` function.", + "type": "minor" } ], "packageName": "@rushstack/node-core-library", diff --git a/libraries/node-core-library/src/JsonFile.ts b/libraries/node-core-library/src/JsonFile.ts index 5eb144225bb..8b3b8ca5375 100644 --- a/libraries/node-core-library/src/JsonFile.ts +++ b/libraries/node-core-library/src/JsonFile.ts @@ -570,8 +570,7 @@ function _formatJsonHeaderComment(headerComment: string): string { if (headerComment === '') { return ''; } - const lines: string[] = headerComment.split('\n'); - const result: string[] = []; + const lines: string[] = Text.convertToLf(headerComment).split('\n'); for (const line of lines) { if (!/^\s*$/.test(line) && !/^\s*\/\//.test(line)) { throw new Error( @@ -580,7 +579,6 @@ function _formatJsonHeaderComment(headerComment: string): string { JSON.stringify(line) ); } - result.push(line.replaceAll('\r', '')); } return lines.join('\n') + '\n'; } diff --git a/libraries/node-core-library/src/test/Executable.test.ts b/libraries/node-core-library/src/test/Executable.test.ts index d3e3c7187d7..9c4f7af1f9a 100644 --- a/libraries/node-core-library/src/test/Executable.test.ts +++ b/libraries/node-core-library/src/test/Executable.test.ts @@ -5,6 +5,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import type * as child_process from 'node:child_process'; import { once } from 'node:events'; +import { Readable } from 'node:stream'; import { Executable, @@ -16,8 +17,8 @@ import { type IWaitForExitResultWithoutOutput } from '../Executable'; import { FileSystem } from '../FileSystem'; +import { Path } from '../Path'; import { PosixModeBits } from '../PosixModeBits'; -import { Readable } from 'node:stream'; describe('Executable process tests', () => { // The PosixModeBits are intended to be used with bitwise operations. @@ -100,7 +101,7 @@ describe('Executable process tests', () => { test('Executable.tryResolve() pathless', () => { const resolved: string | undefined = Executable.tryResolve('npm-binary-wrapper', options); expect(resolved).toBeDefined(); - const resolvedRelative: string = path.relative(executableFolder, resolved!).replaceAll('\\', '/'); + const resolvedRelative: string = Path.convertToSlashes(path.relative(executableFolder, resolved!)); if (os.platform() === 'win32') { // On Windows, we should find npm-binary-wrapper.cmd instead of npm-binary-wrapper diff --git a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts index 3a5a240e91c..4c99d470dea 100644 --- a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts @@ -11,7 +11,7 @@ import { EnvironmentConfiguration } from '../EnvironmentConfiguration'; import { DependencyType } from '../PackageJsonEditor'; function normalizePathForComparison(pathToNormalize: string): string { - return pathToNormalize.replaceAll('\\', '/').toUpperCase(); + return Path.convertToSlashes(pathToNormalize).toUpperCase(); } function assertPathProperty(validatedPropertyName: string, absolutePath: string, relativePath: string): void { diff --git a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts index cf9e51be470..ff64f638de6 100644 --- a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts @@ -13,7 +13,8 @@ import { FileConstants, Sort, InternalError, - AlreadyReportedError + AlreadyReportedError, + Path } from '@rushstack/node-core-library'; import { Colorize, PrintUtilities } from '@rushstack/terminal'; @@ -557,7 +558,8 @@ export class RushInstallManager extends BaseInstallManager { // eslint-disable-next-line no-console console.log(`Deleting ${pathToDeleteWithoutStar}\\*`); // Glob can't handle Windows paths - const normalizedPathToDeleteWithoutStar: string = pathToDeleteWithoutStar.replaceAll('\\', '/'); + const normalizedPathToDeleteWithoutStar: string = + Path.convertToSlashes(pathToDeleteWithoutStar); const { default: glob } = await import('fast-glob'); const tempModulePaths: string[] = await glob( @@ -688,7 +690,7 @@ export class RushInstallManager extends BaseInstallManager { RushConstants.rushTempNpmScope ); // Glob can't handle Windows paths - const normalizedPathToDeleteWithoutStar: string = pathToDeleteWithoutStar.replaceAll('\\', '/'); + const normalizedPathToDeleteWithoutStar: string = Path.convertToSlashes(pathToDeleteWithoutStar); let anyChanges: boolean = false; diff --git a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts index df76928fe70..a9de80dde28 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts @@ -3,6 +3,7 @@ import type * as child_process from 'node:child_process'; +import { Path } from '@rushstack/node-core-library'; import { type ITerminal, type ITerminalProvider, TerminalProviderSeverity } from '@rushstack/terminal'; import type { IPhase } from '../../api/CommandLineConfiguration'; @@ -199,7 +200,7 @@ export function convertSlashesForWindows(command: string): string { // "bin\blarg --path ./config/blah.json && a/b" // // NOTE: we don't attempt to process the path parameter or stuff after "&&" - return commandPart.replaceAll('/', '\\') + remainder; + return Path.convertToBackslashes(commandPart) + remainder; } } From e488c02be673706153fa15f4f3b05684a84c78dd Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:53:48 -0700 Subject: [PATCH 4/4] chore: remove implementation-only change entry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8ebd5bf2-c44b-42d5-be25-e7936d4b0a14 --- .../native-standard-apis_2026-08-18-03-14-05.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json diff --git a/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json b/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json deleted file mode 100644 index 22fad0f037d..00000000000 --- a/common/changes/@rushstack/lockfile-explorer/native-standard-apis_2026-08-18-03-14-05.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "packageName": "@rushstack/lockfile-explorer", - "comment": "Use `AbortSignal.timeout()` for npm registry request timeouts.", - "type": "patch" - } - ], - "packageName": "@rushstack/lockfile-explorer", - "email": "5100938+bmiddha@users.noreply.github.com" -}