diff --git a/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts b/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts index 939f9aa5ba2..ad01475144c 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 = Path.convertToSlashes(scrubbedPath); 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/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/apps/rundown/src/Rundown.ts b/apps/rundown/src/Rundown.ts index 9b63ac8438f..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, Text } 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(Text.replaceAll(relativePath, '\\', '/')); + 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/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..8bef7a52cb5 --- /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": "Deprecate `Text.replaceAll()` in favor of the native `String.prototype.replaceAll()` function.", + "type": "minor" + } + ], + "packageName": "@rushstack/node-core-library", + "email": "5100938+bmiddha@users.noreply.github.com" +} 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..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(Text.replaceAll(line, '\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 465c240c5c0..23fbf483881 100644 --- a/libraries/node-core-library/src/Text.ts +++ b/libraries/node-core-library/src/Text.ts @@ -97,14 +97,21 @@ 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 + * + * @deprecated Use `String.prototype.replaceAll()` instead. */ public static replaceAll(input: string, searchValue: string, replaceValue: string): string { - return input.split(searchValue).join(replaceValue); + return input.replaceAll(searchValue, replaceValue); } /** diff --git a/libraries/node-core-library/src/test/Executable.test.ts b/libraries/node-core-library/src/test/Executable.test.ts index df8e3d9003f..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,9 +17,8 @@ import { type IWaitForExitResultWithoutOutput } from '../Executable'; import { FileSystem } from '../FileSystem'; +import { Path } from '../Path'; import { PosixModeBits } from '../PosixModeBits'; -import { Text } from '../Text'; -import { Readable } from 'node:stream'; describe('Executable process tests', () => { // The PosixModeBits are intended to be used with bitwise operations. @@ -101,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 = Text.replaceAll(path.relative(executableFolder, resolved!), '\\', '/'); + 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 039dbb7df31..4c99d470dea 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 Path.convertToSlashes(pathToNormalize).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..ff64f638de6 100644 --- a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts @@ -9,12 +9,12 @@ import * as ssri from 'ssri'; import { JsonFile, - Text, FileSystem, FileConstants, Sort, InternalError, - AlreadyReportedError + AlreadyReportedError, + Path } from '@rushstack/node-core-library'; import { Colorize, PrintUtilities } from '@rushstack/terminal'; @@ -558,11 +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 = Text.replaceAll( - pathToDeleteWithoutStar, - '\\', - '/' - ); + const normalizedPathToDeleteWithoutStar: string = + Path.convertToSlashes(pathToDeleteWithoutStar); const { default: glob } = await import('fast-glob'); const tempModulePaths: string[] = await glob( @@ -693,7 +690,7 @@ export class RushInstallManager extends BaseInstallManager { RushConstants.rushTempNpmScope ); // Glob can't handle Windows paths - const normalizedPathToDeleteWithoutStar: string = Text.replaceAll(pathToDeleteWithoutStar, '\\', '/'); + 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 c4b368ed38a..a9de80dde28 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts @@ -3,7 +3,7 @@ import type * as child_process from 'node:child_process'; -import { Text } from '@rushstack/node-core-library'; +import { Path } from '@rushstack/node-core-library'; import { type ITerminal, type ITerminalProvider, TerminalProviderSeverity } from '@rushstack/terminal'; import type { IPhase } from '../../api/CommandLineConfiguration'; @@ -200,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 Text.replaceAll(commandPart, '/', '\\') + remainder; + return Path.convertToBackslashes(commandPart) + 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(); });