From dcae0f91aaea0e7828cbe6127b0925684c1ddd66 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 15 Oct 2025 23:44:25 +0000 Subject: [PATCH 01/14] Add new credential-cache package --- .../rush/nonbrowser-approved-packages.json | 4 +++ libraries/credential-cache/.npmignore | 32 +++++++++++++++++++ libraries/credential-cache/LICENSE | 24 ++++++++++++++ libraries/credential-cache/README.md | 17 ++++++++++ .../config/api-extractor.json | 19 +++++++++++ libraries/credential-cache/config/rig.json | 7 ++++ libraries/credential-cache/eslint.config.js | 18 +++++++++++ libraries/credential-cache/package.json | 25 +++++++++++++++ libraries/credential-cache/src/index.ts | 10 ++++++ libraries/credential-cache/tsconfig.json | 3 ++ libraries/rush-lib/package.json | 1 + rush.json | 6 ++++ 12 files changed, 166 insertions(+) create mode 100644 libraries/credential-cache/.npmignore create mode 100644 libraries/credential-cache/LICENSE create mode 100644 libraries/credential-cache/README.md create mode 100644 libraries/credential-cache/config/api-extractor.json create mode 100644 libraries/credential-cache/config/rig.json create mode 100644 libraries/credential-cache/eslint.config.js create mode 100644 libraries/credential-cache/package.json create mode 100644 libraries/credential-cache/src/index.ts create mode 100644 libraries/credential-cache/tsconfig.json diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 90bf63302d7..161327d854d 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -130,6 +130,10 @@ "name": "@rspack/dev-server", "allowedCategories": [ "libraries" ] }, + { + "name": "@rushstack/credential-cache", + "allowedCategories": [ "libraries" ] + }, { "name": "@rushstack/debug-certificate-manager", "allowedCategories": [ "libraries", "vscode-extensions" ] diff --git a/libraries/credential-cache/.npmignore b/libraries/credential-cache/.npmignore new file mode 100644 index 00000000000..bc349f9a4be --- /dev/null +++ b/libraries/credential-cache/.npmignore @@ -0,0 +1,32 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README.md +# LICENSE + +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- diff --git a/libraries/credential-cache/LICENSE b/libraries/credential-cache/LICENSE new file mode 100644 index 00000000000..a8687a8508f --- /dev/null +++ b/libraries/credential-cache/LICENSE @@ -0,0 +1,24 @@ +@rushstack/credential-cache + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/libraries/credential-cache/README.md b/libraries/credential-cache/README.md new file mode 100644 index 00000000000..058aa227884 --- /dev/null +++ b/libraries/credential-cache/README.md @@ -0,0 +1,17 @@ +# @rushstack/credential-cache + +## Installation + +`npm install @rushstack/credential-cache --save-dev` + +## Overview + + +## Links + +- [CHANGELOG.md]( + https://github.com/microsoft/rushstack/blob/main/libraries/credential-cache/CHANGELOG.md) - Find + out what's new in the latest version +- [API Reference](https://api.rushstack.io/pages/credential-cache/) + +**@rushstack/credential-cache** is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/libraries/credential-cache/config/api-extractor.json b/libraries/credential-cache/config/api-extractor.json new file mode 100644 index 00000000000..996e271d3dd --- /dev/null +++ b/libraries/credential-cache/config/api-extractor.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/lib/index.d.ts", + + "apiReport": { + "enabled": true, + "reportFolder": "../../../common/reviews/api" + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "../../../common/temp/api/.api.json" + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/libraries/credential-cache/config/rig.json b/libraries/credential-cache/config/rig.json new file mode 100644 index 00000000000..165ffb001f5 --- /dev/null +++ b/libraries/credential-cache/config/rig.json @@ -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" +} diff --git a/libraries/credential-cache/eslint.config.js b/libraries/credential-cache/eslint.config.js new file mode 100644 index 00000000000..c15e6077310 --- /dev/null +++ b/libraries/credential-cache/eslint.config.js @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const nodeTrustedToolProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node-trusted-tool'); +const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals'); + +module.exports = [ + ...nodeTrustedToolProfile, + ...friendlyLocalsMixin, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + } + } +]; diff --git a/libraries/credential-cache/package.json b/libraries/credential-cache/package.json new file mode 100644 index 00000000000..d5fc7e2092b --- /dev/null +++ b/libraries/credential-cache/package.json @@ -0,0 +1,25 @@ +{ + "name": "@rushstack/credential-cache", + "version": "0.0.0", + "description": "Cross-platform functionality to manage cached credentials.", + "main": "lib/index.js", + "typings": "dist/credential-cache.d.ts", + "license": "MIT", + "repository": { + "url": "https://github.com/microsoft/rushstack.git", + "type": "git", + "directory": "libraries/credential-cache" + }, + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean" + }, + "dependencies": { + "@rushstack/node-core-library": "workspace:*" + }, + "devDependencies": { + "@rushstack/heft": "workspace:*", + "eslint": "~9.37.0", + "local-node-rig": "workspace:*" + } +} diff --git a/libraries/credential-cache/src/index.ts b/libraries/credential-cache/src/index.ts new file mode 100644 index 00000000000..16fcdc3f007 --- /dev/null +++ b/libraries/credential-cache/src/index.ts @@ -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. + +/** + * This package is used to manage persistent, per-user cached credentials. + * + * @packageDocumentation + */ + +export { CredentialCache } from './CredentialCache'; diff --git a/libraries/credential-cache/tsconfig.json b/libraries/credential-cache/tsconfig.json new file mode 100644 index 00000000000..dac21d04081 --- /dev/null +++ b/libraries/credential-cache/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" +} diff --git a/libraries/rush-lib/package.json b/libraries/rush-lib/package.json index 27769a34588..99563d59046 100644 --- a/libraries/rush-lib/package.json +++ b/libraries/rush-lib/package.json @@ -33,6 +33,7 @@ "@pnpm/dependency-path-lockfile-pre-v10": "npm:@pnpm/dependency-path@~5.1.7", "@pnpm/dependency-path": "~1000.0.9", "@pnpm/link-bins": "~5.3.7", + "@rushstack/credential-cache": "workspace:*", "@rushstack/heft-config-file": "workspace:*", "@rushstack/lookup-by-path": "workspace:*", "@rushstack/node-core-library": "workspace:*", diff --git a/rush.json b/rush.json index 0cb4ae72f2e..a3ff7deea8e 100644 --- a/rush.json +++ b/rush.json @@ -1154,6 +1154,12 @@ "shouldPublish": true, "decoupledLocalDependencies": ["@rushstack/heft"] }, + { + "packageName": "@rushstack/credential-cache", + "projectFolder": "libraries/credential-cache", + "reviewCategory": "libraries", + "shouldPublish": true + }, { "packageName": "@rushstack/debug-certificate-manager", "projectFolder": "libraries/debug-certificate-manager", From b0d4184a1742244fe7e6da66327fe1d001453f46 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 15 Oct 2025 23:48:17 +0000 Subject: [PATCH 02/14] Add test command --- libraries/credential-cache/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/credential-cache/package.json b/libraries/credential-cache/package.json index d5fc7e2092b..b8465bba555 100644 --- a/libraries/credential-cache/package.json +++ b/libraries/credential-cache/package.json @@ -12,7 +12,8 @@ }, "scripts": { "build": "heft build --clean", - "_phase:build": "heft run --only build -- --clean" + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" }, "dependencies": { "@rushstack/node-core-library": "workspace:*" From 59023ee66fae5dd5ae402120c5b153de36ed6a5f Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 01:56:42 +0000 Subject: [PATCH 03/14] Factor out CredentialCache --- ...act-credential-cache_2025-10-22-01-44.json | 10 + ...act-credential-cache_2025-10-22-01-44.json | 10 + ...act-credential-cache_2025-10-22-01-44.json | 10 + common/reviews/api/credential-cache.api.md | 49 ++++ common/reviews/api/node-core-library.api.md | 28 ++- common/reviews/api/rush-lib.api.md | 40 +--- .../credential-cache/src/CredentialCache.ts | 215 ++++++++++++++++++ libraries/credential-cache/src/index.ts | 2 +- .../src/schemas/credentials.schema.json | 36 +++ .../src/test/CredentialCache.mock.ts | 9 + .../src}/test/CredentialCache.test.ts | 7 +- .../CredentialCache.test.ts.snap | 0 libraries/node-core-library/src/Objects.ts | 6 + libraries/node-core-library/src/User.ts | 6 + libraries/node-core-library/src/index.ts | 108 ++++++--- .../src/objects/areDeepEqual.ts | 63 +++++ .../node-core-library/src/objects/index.ts | 4 + .../src/objects/test/areDeepEqual.test.ts | 77 +++++++ .../src/user/getHomeFolder.ts | 27 +++ libraries/node-core-library/src/user/index.ts | 4 + libraries/rush-lib/src/api/FlagFile.ts | 6 +- libraries/rush-lib/src/api/LastInstallFlag.ts | 4 +- .../rush-lib/src/api/RushGlobalFolder.ts | 5 +- .../rush-lib/src/api/RushUserConfiguration.ts | 5 +- .../src/cli/RushPnpmCommandLineParser.ts | 6 +- .../rush-lib/src/logic/CredentialCache.ts | 211 +---------------- .../installManager/WorkspaceInstallManager.ts | 6 +- .../src/logic/setup/SetupPackageRegistry.ts | 5 +- .../rush-lib/src/utilities/objectUtilities.ts | 58 +---- .../utilities/test/objectUtilities.test.ts | 75 +----- libraries/rush-sdk/package.json | 1 + .../package.json | 1 + .../src/AmazonS3BuildCacheProvider.ts | 3 +- .../test/AmazonS3BuildCacheProvider.test.ts | 8 +- .../package.json | 1 + .../src/AzureAuthenticationBase.ts | 4 +- .../AzureStorageBuildCacheProvider.test.ts | 3 +- .../rush-http-build-cache-plugin/package.json | 1 + .../src/HttpBuildCacheProvider.ts | 3 +- 39 files changed, 671 insertions(+), 446 deletions(-) create mode 100644 common/changes/@microsoft/rush/extract-credential-cache_2025-10-22-01-44.json create mode 100644 common/changes/@rushstack/credential-cache/extract-credential-cache_2025-10-22-01-44.json create mode 100644 common/changes/@rushstack/node-core-library/extract-credential-cache_2025-10-22-01-44.json create mode 100644 common/reviews/api/credential-cache.api.md create mode 100644 libraries/credential-cache/src/CredentialCache.ts create mode 100644 libraries/credential-cache/src/schemas/credentials.schema.json create mode 100644 libraries/credential-cache/src/test/CredentialCache.mock.ts rename libraries/{rush-lib/src/logic => credential-cache/src}/test/CredentialCache.test.ts (98%) rename libraries/{rush-lib/src/logic => credential-cache/src}/test/__snapshots__/CredentialCache.test.ts.snap (100%) create mode 100644 libraries/node-core-library/src/Objects.ts create mode 100644 libraries/node-core-library/src/User.ts create mode 100644 libraries/node-core-library/src/objects/areDeepEqual.ts create mode 100644 libraries/node-core-library/src/objects/index.ts create mode 100644 libraries/node-core-library/src/objects/test/areDeepEqual.test.ts create mode 100644 libraries/node-core-library/src/user/getHomeFolder.ts create mode 100644 libraries/node-core-library/src/user/index.ts diff --git a/common/changes/@microsoft/rush/extract-credential-cache_2025-10-22-01-44.json b/common/changes/@microsoft/rush/extract-credential-cache_2025-10-22-01-44.json new file mode 100644 index 00000000000..dc9077e574f --- /dev/null +++ b/common/changes/@microsoft/rush/extract-credential-cache_2025-10-22-01-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Extract CredentialCache API out into \"@rushstack/credential-cache\". Reference directly in plugins to avoid pulling in all of \"@rushstack/rush-sdk\" unless necessary.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@rushstack/credential-cache/extract-credential-cache_2025-10-22-01-44.json b/common/changes/@rushstack/credential-cache/extract-credential-cache_2025-10-22-01-44.json new file mode 100644 index 00000000000..2a2ea4de55e --- /dev/null +++ b/common/changes/@rushstack/credential-cache/extract-credential-cache_2025-10-22-01-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/credential-cache", + "comment": "Create dedicated package for the Rush \"CredentialCache\" API. This API manages credential persistence on the local machine.", + "type": "minor" + } + ], + "packageName": "@rushstack/credential-cache" +} \ No newline at end of file diff --git a/common/changes/@rushstack/node-core-library/extract-credential-cache_2025-10-22-01-44.json b/common/changes/@rushstack/node-core-library/extract-credential-cache_2025-10-22-01-44.json new file mode 100644 index 00000000000..4e750208dbe --- /dev/null +++ b/common/changes/@rushstack/node-core-library/extract-credential-cache_2025-10-22-01-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Add \"Objects.areDeepEqual\" and \"User.getHomeFolder\" APIs.", + "type": "minor" + } + ], + "packageName": "@rushstack/node-core-library" +} \ No newline at end of file diff --git a/common/reviews/api/credential-cache.api.md b/common/reviews/api/credential-cache.api.md new file mode 100644 index 00000000000..fa2c0647447 --- /dev/null +++ b/common/reviews/api/credential-cache.api.md @@ -0,0 +1,49 @@ +## API Report File for "@rushstack/credential-cache" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export class CredentialCache implements Disposable { + // Warning: (ae-forgotten-export) The symbol "SYMBOL_DISPOSE" needs to be exported by the entry point index.d.ts + // + // (undocumented) + [SYMBOL_DISPOSE](): void; + // (undocumented) + deleteCacheEntry(cacheId: string): void; + // (undocumented) + dispose(): void; + // (undocumented) + static initializeAsync(options: ICredentialCacheOptions): Promise; + // (undocumented) + saveIfModifiedAsync(): Promise; + // (undocumented) + setCacheEntry(cacheId: string, entry: ICredentialCacheEntry): void; + // (undocumented) + trimExpiredEntries(): void; + // (undocumented) + tryGetCacheEntry(cacheId: string): ICredentialCacheEntry | undefined; + // (undocumented) + static usingAsync(options: ICredentialCacheOptions, doActionAsync: (credentialCache: CredentialCache) => Promise | void): Promise; +} + +// @public (undocumented) +export interface ICredentialCacheEntry { + // (undocumented) + credential: string; + // (undocumented) + credentialMetadata?: object; + // (undocumented) + expires?: Date; +} + +// @public (undocumented) +export interface ICredentialCacheOptions { + // (undocumented) + cacheFilePath?: string; + // (undocumented) + supportEditing: boolean; +} + +``` diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 6133072d1c5..a8440caf548 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -7,7 +7,7 @@ /// import * as child_process from 'node:child_process'; -import * as nodeFs from 'node:fs'; +import * as fs from 'node:fs'; import * as nodePath from 'node:path'; // @public @@ -24,6 +24,9 @@ export class AlreadyReportedError extends Error { constructor(); } +// @public +function areDeepEqual(a: TObject, b: TObject): boolean; + // @public export class Async { static forEachAsync(iterable: Iterable | AsyncIterable, callback: (entry: TEntry, arrayIndex: number) => Promise, options?: (IAsyncParallelismOptions & { @@ -216,7 +219,7 @@ export type FileSystemCopyFilesAsyncFilter = (sourcePath: string, destinationPat export type FileSystemCopyFilesFilter = (sourcePath: string, destinationPath: string) => boolean; // @public -export type FileSystemStats = nodeFs.Stats; +export type FileSystemStats = fs.Stats; // @public export class FileWriter { @@ -234,7 +237,10 @@ export const FolderConstants: { }; // @public -export type FolderItem = nodeFs.Dirent; +export type FolderItem = fs.Dirent; + +// @public +function getHomeFolder(): string; // @public export interface IAsyncParallelismOptions { @@ -628,7 +634,7 @@ export interface IReadLinesFromIterableOptions { // @public export interface IRealNodeModulePathResolverOptions { // (undocumented) - fs?: Partial>; + fs?: Partial>; ignoreMissingPaths?: boolean; // (undocumented) path?: Partial>; @@ -791,6 +797,13 @@ export enum NewlineKind { OsDefault = "os" } +declare namespace Objects { + export { + areDeepEqual + } +} +export { Objects } + // @public export class PackageJsonLookup { constructor(parameters?: IPackageJsonLookupParameters); @@ -932,4 +945,11 @@ export class TypeUuid { static registerClass(targetClass: any, typeUuid: string): void; } +declare namespace User { + export { + getHomeFolder + } +} +export { User } + ``` diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 3fa9d1b178b..d6804daab7e 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -13,7 +13,10 @@ import { AsyncSeriesWaterfallHook } from 'tapable'; import type { CollatedWriter } from '@rushstack/stream-collator'; import type { CommandLineParameter } from '@rushstack/ts-command-line'; import { CommandLineParameterKind } from '@rushstack/ts-command-line'; +import { CredentialCache } from '@rushstack/credential-cache'; import { HookMap } from 'tapable'; +import { ICredentialCacheEntry } from '@rushstack/credential-cache'; +import { ICredentialCacheOptions } from '@rushstack/credential-cache'; import { IFileDiffStatus } from '@rushstack/package-deps-hash'; import { IPackageJson } from '@rushstack/node-core-library'; import { IPrefixMatch } from '@rushstack/lookup-by-path'; @@ -139,25 +142,7 @@ export class CommonVersionsConfiguration { save(): boolean; } -// @beta (undocumented) -export class CredentialCache { - // (undocumented) - deleteCacheEntry(cacheId: string): void; - // (undocumented) - dispose(): void; - // (undocumented) - static initializeAsync(options: ICredentialCacheOptions): Promise; - // (undocumented) - saveIfModifiedAsync(): Promise; - // (undocumented) - setCacheEntry(cacheId: string, entry: ICredentialCacheEntry): void; - // (undocumented) - trimExpiredEntries(): void; - // (undocumented) - tryGetCacheEntry(cacheId: string): ICredentialCacheEntry | undefined; - // (undocumented) - static usingAsync(options: ICredentialCacheOptions, doActionAsync: (credentialCache: CredentialCache) => Promise | void): Promise; -} +export { CredentialCache } // @beta export enum CustomTipId { @@ -433,22 +418,9 @@ export interface ICreateOperationsContext { readonly rushConfiguration: RushConfiguration; } -// @beta (undocumented) -export interface ICredentialCacheEntry { - // (undocumented) - credential: string; - // (undocumented) - credentialMetadata?: object; - // (undocumented) - expires?: Date; -} +export { ICredentialCacheEntry } -// @beta (undocumented) -export interface ICredentialCacheOptions { - cacheFilePath?: string; - // (undocumented) - supportEditing: boolean; -} +export { ICredentialCacheOptions } // @beta export interface ICustomTipInfo { diff --git a/libraries/credential-cache/src/CredentialCache.ts b/libraries/credential-cache/src/CredentialCache.ts new file mode 100644 index 00000000000..606f01c2a8f --- /dev/null +++ b/libraries/credential-cache/src/CredentialCache.ts @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; + +import { FileSystem, JsonFile, JsonSchema, LockFile, User, Objects } from '@rushstack/node-core-library'; + +import schemaJson from './schemas/credentials.schema.json'; + +const DEFAULT_CACHE_FILENAME: 'credentials.json' = 'credentials.json'; +const LATEST_CREDENTIALS_JSON_VERSION: string = '0.1.0'; + +interface ICredentialCacheJson { + version: string; + cacheEntries: { + [credentialCacheId: string]: ICacheEntryJson; + }; +} + +interface ICacheEntryJson { + expires: number; + credential: string; + credentialMetadata?: object; +} + +// Polyfill for node 18 +const SYMBOL_DISPOSE: typeof Symbol.dispose = Symbol.dispose ?? Symbol.for('Symbol.dispose'); + +/** + * @public + */ +export interface ICredentialCacheEntry { + expires?: Date; + credential: string; + credentialMetadata?: object; +} + +/** + * @public + */ +export interface ICredentialCacheOptions { + supportEditing: boolean; + cacheFilePath?: string; +} + +/** + * @public + */ +export class CredentialCache implements Disposable { + private readonly _cacheFilePath: string; + private readonly _cacheEntries: Map; + private _modified: boolean = false; + private _disposed: boolean = false; + private readonly _supportsEditing: boolean; + private readonly _lockfile: LockFile | undefined; + + private constructor( + cacheFilePath: string, + loadedJson: ICredentialCacheJson | undefined, + lockfile: LockFile | undefined + ) { + if (loadedJson && loadedJson.version !== LATEST_CREDENTIALS_JSON_VERSION) { + throw new Error(`Unexpected ${cacheFilePath} file version: ${loadedJson.version}`); + } + + this._cacheFilePath = cacheFilePath; + this._cacheEntries = new Map(Object.entries(loadedJson?.cacheEntries || {})); + this._supportsEditing = !!lockfile; + this._lockfile = lockfile; + } + + public static async initializeAsync(options: ICredentialCacheOptions): Promise { + let cacheDirectory: string; + let cacheFileName: string; + if (options.cacheFilePath) { + cacheDirectory = path.dirname(options.cacheFilePath); + cacheFileName = options.cacheFilePath.slice(cacheDirectory.length + 1); + } else { + cacheDirectory = `${User.getHomeFolder()}/.rush-user`; + cacheFileName = DEFAULT_CACHE_FILENAME; + } + const cacheFilePath: string = `${cacheDirectory}/${cacheFileName}`; + + const jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson); + + let loadedJson: ICredentialCacheJson | undefined; + try { + loadedJson = await JsonFile.loadAndValidateAsync(cacheFilePath, jsonSchema); + } catch (e) { + if (!FileSystem.isErrnoException(e as Error)) { + throw e; + } + } + + let lockfile: LockFile | undefined; + if (options.supportEditing) { + lockfile = await LockFile.acquireAsync(cacheDirectory, `${cacheFileName}.lock`); + } + + const credentialCache: CredentialCache = new CredentialCache(cacheFilePath, loadedJson, lockfile); + return credentialCache; + } + + public static async usingAsync( + options: ICredentialCacheOptions, + doActionAsync: (credentialCache: CredentialCache) => Promise | void + ): Promise { + const cache: CredentialCache = await CredentialCache.initializeAsync(options); + try { + await doActionAsync(cache); + } finally { + cache.dispose(); + } + } + + public setCacheEntry(cacheId: string, entry: ICredentialCacheEntry): void { + this._validate(true); + + const { expires, credential, credentialMetadata } = entry; + const expiresMilliseconds: number = expires?.getTime() || 0; + const existingCacheEntry: ICacheEntryJson | undefined = this._cacheEntries.get(cacheId); + if ( + existingCacheEntry?.credential !== credential || + existingCacheEntry?.expires !== expiresMilliseconds || + !Objects.areDeepEqual(existingCacheEntry?.credentialMetadata, credentialMetadata) + ) { + this._modified = true; + this._cacheEntries.set(cacheId, { + expires: expiresMilliseconds, + credential, + credentialMetadata + }); + } + } + + public tryGetCacheEntry(cacheId: string): ICredentialCacheEntry | undefined { + this._validate(false); + + const cacheEntry: ICacheEntryJson | undefined = this._cacheEntries.get(cacheId); + if (cacheEntry) { + const result: ICredentialCacheEntry = { + expires: cacheEntry.expires ? new Date(cacheEntry.expires) : undefined, + credential: cacheEntry.credential, + credentialMetadata: cacheEntry.credentialMetadata + }; + + return result; + } else { + return undefined; + } + } + + public deleteCacheEntry(cacheId: string): void { + this._validate(true); + + if (this._cacheEntries.has(cacheId)) { + this._modified = true; + this._cacheEntries.delete(cacheId); + } + } + + public trimExpiredEntries(): void { + this._validate(true); + + const now: number = Date.now(); + for (const [cacheId, cacheEntry] of this._cacheEntries.entries()) { + if (cacheEntry.expires < now) { + this._cacheEntries.delete(cacheId); + this._modified = true; + } + } + } + + public async saveIfModifiedAsync(): Promise { + this._validate(true); + + if (this._modified) { + const cacheEntriesJson: { [cacheId: string]: ICacheEntryJson } = {}; + for (const [cacheId, cacheEntry] of this._cacheEntries.entries()) { + cacheEntriesJson[cacheId] = cacheEntry; + } + + const newJson: ICredentialCacheJson = { + version: LATEST_CREDENTIALS_JSON_VERSION, + cacheEntries: cacheEntriesJson + }; + await JsonFile.saveAsync(newJson, this._cacheFilePath, { + ensureFolderExists: true, + updateExistingFile: true, + ignoreUndefinedValues: true + }); + + this._modified = false; + } + } + + public [SYMBOL_DISPOSE](): void { + this.dispose(); + } + + public dispose(): void { + this._lockfile?.release(); + this._disposed = true; + } + + private _validate(requiresEditing: boolean): void { + if (!this._supportsEditing && requiresEditing) { + throw new Error(`This instance of ${CredentialCache.name} does not support editing.`); + } + + if (this._disposed) { + throw new Error(`This instance of ${CredentialCache.name} has been disposed.`); + } + } +} diff --git a/libraries/credential-cache/src/index.ts b/libraries/credential-cache/src/index.ts index 16fcdc3f007..6c5470d8ed1 100644 --- a/libraries/credential-cache/src/index.ts +++ b/libraries/credential-cache/src/index.ts @@ -7,4 +7,4 @@ * @packageDocumentation */ -export { CredentialCache } from './CredentialCache'; +export { CredentialCache, type ICredentialCacheEntry, type ICredentialCacheOptions } from './CredentialCache'; diff --git a/libraries/credential-cache/src/schemas/credentials.schema.json b/libraries/credential-cache/src/schemas/credentials.schema.json new file mode 100644 index 00000000000..9800b511877 --- /dev/null +++ b/libraries/credential-cache/src/schemas/credentials.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Cache for credentials used with the Rush tool.", + "description": "For use with the Rush tool, this file acts as a cache for the credentials. See http://rushjs.io for details.", + + "type": "object", + + "required": ["version", "cacheEntries"], + "properties": { + "version": { + "type": "string" + }, + + "cacheEntries": { + "type": "object", + "patternProperties": { + ".+": { + "type": "object", + "required": ["expires", "credential"], + + "properties": { + "expires": { + "type": "number" + }, + "credential": { + "type": "string" + }, + "credentialMetadata": { + "type": "object" + } + } + } + } + } + } +} diff --git a/libraries/credential-cache/src/test/CredentialCache.mock.ts b/libraries/credential-cache/src/test/CredentialCache.mock.ts new file mode 100644 index 00000000000..5a4bcc29188 --- /dev/null +++ b/libraries/credential-cache/src/test/CredentialCache.mock.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as GetHomeFolderModule from '@rushstack/node-core-library/lib/user/getHomeFolder'; + +export const mockGetHomeFolder: jest.MockedFunction = jest.fn(); +jest.mock('@rushstack/node-core-library/lib/user/getHomeFolder', (): typeof GetHomeFolderModule => ({ + getHomeFolder: mockGetHomeFolder +})); diff --git a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts b/libraries/credential-cache/src/test/CredentialCache.test.ts similarity index 98% rename from libraries/rush-lib/src/logic/test/CredentialCache.test.ts rename to libraries/credential-cache/src/test/CredentialCache.test.ts index 46ce1bf6ccd..29b96a1135c 100644 --- a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts +++ b/libraries/credential-cache/src/test/CredentialCache.test.ts @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { mockGetHomeFolder } from './CredentialCache.mock'; import { LockFile, Async, FileSystem } from '@rushstack/node-core-library'; -import { RushUserConfiguration } from '../../api/RushUserConfiguration'; import { CredentialCache, type ICredentialCacheOptions } from '../CredentialCache'; -const FAKE_RUSH_USER_FOLDER: string = 'temp/.rush-user'; +const FAKE_HOME_FOLDER: string = 'temp'; +const FAKE_RUSH_USER_FOLDER: string = `${FAKE_HOME_FOLDER}/.rush-user`; interface IPathsTestCase extends Required> { testCaseName: string; @@ -23,7 +24,7 @@ describe(CredentialCache.name, () => { }); beforeEach(() => { - jest.spyOn(RushUserConfiguration, 'getRushUserFolderPath').mockReturnValue(FAKE_RUSH_USER_FOLDER); + mockGetHomeFolder.mockReturnValue(FAKE_HOME_FOLDER); // TODO: Consider expanding these mocks and moving them to node-core-library jest diff --git a/libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap b/libraries/credential-cache/src/test/__snapshots__/CredentialCache.test.ts.snap similarity index 100% rename from libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap rename to libraries/credential-cache/src/test/__snapshots__/CredentialCache.test.ts.snap diff --git a/libraries/node-core-library/src/Objects.ts b/libraries/node-core-library/src/Objects.ts new file mode 100644 index 00000000000..ac7923db5f7 --- /dev/null +++ b/libraries/node-core-library/src/Objects.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as Objects from './objects/index'; + +export { Objects }; diff --git a/libraries/node-core-library/src/User.ts b/libraries/node-core-library/src/User.ts new file mode 100644 index 00000000000..3107da72bf6 --- /dev/null +++ b/libraries/node-core-library/src/User.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as User from './user/index'; + +export { User }; diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index 648b559353c..7c2e32dfc10 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -9,7 +9,10 @@ * @packageDocumentation */ +export type { IProblemPattern } from '@rushstack/problem-matcher'; + export { AlreadyReportedError } from './AlreadyReportedError'; + export { Async, AsyncQueue, @@ -18,10 +21,13 @@ export { type IRunWithTimeoutOptions, type IWeighted } from './Async'; -export type { Brand } from './PrimitiveTypes'; + export { FileConstants, FolderConstants } from './Constants'; + export { Enum } from './Enum'; + export { EnvironmentMap, type IEnvironmentEntry } from './EnvironmentMap'; + export { type ExecutableStdioStreamMapping, type ExecutableStdioMapping, @@ -36,18 +42,32 @@ export { type IProcessInfo, Executable } from './Executable'; + export { type IFileErrorOptions, type IFileErrorFormattingOptions, FileError } from './FileError'; -export type { IProblemPattern } from '@rushstack/problem-matcher'; -export type { - INodePackageJson, - IPackageJson, - IPackageJsonDependencyTable, - IPackageJsonScriptTable, - IPackageJsonRepository, - IPeerDependenciesMetaTable, - IDependenciesMetaTable, - IPackageJsonExports -} from './IPackageJson'; + +export { + AlreadyExistsBehavior, + FileSystem, + type FileSystemCopyFilesAsyncFilter, + type FileSystemCopyFilesFilter, + type FolderItem, + type FileSystemStats, + type IFileSystemCopyFileBaseOptions, + type IFileSystemCopyFileOptions, + type IFileSystemCopyFilesAsyncOptions, + type IFileSystemCopyFilesOptions, + type IFileSystemCreateLinkOptions, + type IFileSystemDeleteFileOptions, + type IFileSystemMoveOptions, + type IFileSystemReadFileOptions, + type IFileSystemReadFolderOptions, + type IFileSystemUpdateTimeParameters, + type IFileSystemWriteBinaryFileOptions, + type IFileSystemWriteFileOptions +} from './FileSystem'; + +export { FileWriter, type IFileWriterFlags } from './FileWriter'; + export { Import, type IImportResolveOptions, @@ -57,7 +77,20 @@ export { type IImportResolvePackageOptions, type IImportResolvePackageAsyncOptions } from './Import'; + export { InternalError } from './InternalError'; + +export type { + INodePackageJson, + IPackageJson, + IPackageJsonDependencyTable, + IPackageJsonScriptTable, + IPackageJsonRepository, + IPeerDependenciesMetaTable, + IDependenciesMetaTable, + IPackageJsonExports +} from './IPackageJson'; + export { type JsonObject, type JsonNull, @@ -68,6 +101,7 @@ export { type IJsonFileSaveOptions, JsonFile } from './JsonFile'; + export { type IJsonSchemaErrorInfo, type IJsonSchemaCustomFormat, @@ -79,12 +113,19 @@ export { JsonSchema, type JsonSchemaVersion } from './JsonSchema'; + +export { LegacyAdapters, type LegacyCallback } from './LegacyAdapters'; + export { LockFile } from './LockFile'; + export { MapExtensions } from './MapExtensions'; + export { MinimumHeap } from './MinimumHeap'; -export { PosixModeBits } from './PosixModeBits'; -export { ProtectableMap, type IProtectableMapParameters } from './ProtectableMap'; + +export { Objects } from './Objects'; + export { type IPackageJsonLookupParameters, PackageJsonLookup } from './PackageJsonLookup'; + export { PackageName, PackageNameParser, @@ -92,37 +133,30 @@ export { type IParsedPackageName, type IParsedPackageNameOrError } from './PackageName'; + export { Path, type FileLocationStyle, type IPathFormatFileLocationOptions, type IPathFormatConciselyOptions } from './Path'; + +export { PosixModeBits } from './PosixModeBits'; + +export type { Brand } from './PrimitiveTypes'; + +export { ProtectableMap, type IProtectableMapParameters } from './ProtectableMap'; + export { RealNodeModulePathResolver, type IRealNodeModulePathResolverOptions } from './RealNodeModulePath'; -export { Encoding, Text, NewlineKind, type IReadLinesFromIterableOptions } from './Text'; + export { Sort } from './Sort'; -export { - AlreadyExistsBehavior, - FileSystem, - type FileSystemCopyFilesAsyncFilter, - type FileSystemCopyFilesFilter, - type FolderItem, - type FileSystemStats, - type IFileSystemCopyFileBaseOptions, - type IFileSystemCopyFileOptions, - type IFileSystemCopyFilesAsyncOptions, - type IFileSystemCopyFilesOptions, - type IFileSystemCreateLinkOptions, - type IFileSystemDeleteFileOptions, - type IFileSystemMoveOptions, - type IFileSystemReadFileOptions, - type IFileSystemReadFolderOptions, - type IFileSystemUpdateTimeParameters, - type IFileSystemWriteBinaryFileOptions, - type IFileSystemWriteFileOptions -} from './FileSystem'; -export { FileWriter, type IFileWriterFlags } from './FileWriter'; -export { LegacyAdapters, type LegacyCallback } from './LegacyAdapters'; + export { StringBuilder, type IStringBuilder } from './StringBuilder'; + export { type ISubprocessOptions, SubprocessTerminator } from './SubprocessTerminator'; + +export { Encoding, Text, NewlineKind, type IReadLinesFromIterableOptions } from './Text'; + export { TypeUuid } from './TypeUuid'; + +export { User } from './User'; diff --git a/libraries/node-core-library/src/objects/areDeepEqual.ts b/libraries/node-core-library/src/objects/areDeepEqual.ts new file mode 100644 index 00000000000..09d618e588a --- /dev/null +++ b/libraries/node-core-library/src/objects/areDeepEqual.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Determines if two objects are deeply equal. + * @public + */ +export function areDeepEqual(a: TObject, b: TObject): boolean { + if (a === b) { + return true; + } else { + const aType: string = typeof a; + const bType: string = typeof b; + + if (aType !== bType) { + return false; + } else { + if (aType === 'object') { + if (a === null || b === null) { + // We already handled the case where a === b, so if either is null, they are not equal + return false; + } else if (Array.isArray(a)) { + if (!Array.isArray(b) || a.length !== b.length) { + return false; + } else { + for (let i: number = 0; i < a.length; ++i) { + if (!areDeepEqual(a[i], b[i])) { + return false; + } + } + + return true; + } + } else { + const aObjectProperties: Set = new Set(Object.getOwnPropertyNames(a)); + const bObjectProperties: Set = new Set(Object.getOwnPropertyNames(b)); + if (aObjectProperties.size !== bObjectProperties.size) { + return false; + } else { + for (const property of aObjectProperties) { + if (bObjectProperties.delete(property)) { + if ( + !areDeepEqual( + (a as Record)[property], + (b as Record)[property] + ) + ) { + return false; + } + } else { + return false; + } + } + + return bObjectProperties.size === 0; + } + } + } else { + return false; + } + } + } +} diff --git a/libraries/node-core-library/src/objects/index.ts b/libraries/node-core-library/src/objects/index.ts new file mode 100644 index 00000000000..77fdef00371 --- /dev/null +++ b/libraries/node-core-library/src/objects/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export { areDeepEqual } from './areDeepEqual'; diff --git a/libraries/node-core-library/src/objects/test/areDeepEqual.test.ts b/libraries/node-core-library/src/objects/test/areDeepEqual.test.ts new file mode 100644 index 00000000000..28f29790685 --- /dev/null +++ b/libraries/node-core-library/src/objects/test/areDeepEqual.test.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { areDeepEqual } from '../areDeepEqual'; + +describe(areDeepEqual.name, () => { + it('can compare primitives', () => { + expect(areDeepEqual(1, 1)).toEqual(true); + expect(areDeepEqual(1, undefined)).toEqual(false); + expect(areDeepEqual(1, null)).toEqual(false); + expect(areDeepEqual(undefined, 1)).toEqual(false); + expect(areDeepEqual(null, 1)).toEqual(false); + expect(areDeepEqual(1, 2)).toEqual(false); + + expect(areDeepEqual('a', 'a')).toEqual(true); + expect(areDeepEqual('a', undefined)).toEqual(false); + expect(areDeepEqual('a', null)).toEqual(false); + expect(areDeepEqual(undefined, 'a')).toEqual(false); + expect(areDeepEqual(null, 'a')).toEqual(false); + expect(areDeepEqual('a', 'b')).toEqual(false); + + expect(areDeepEqual(true, true)).toEqual(true); + expect(areDeepEqual(true, undefined)).toEqual(false); + expect(areDeepEqual(true, null)).toEqual(false); + expect(areDeepEqual(undefined, true)).toEqual(false); + expect(areDeepEqual(null, true)).toEqual(false); + expect(areDeepEqual(true, false)).toEqual(false); + + expect(areDeepEqual(undefined, undefined)).toEqual(true); + expect(areDeepEqual(undefined, null)).toEqual(false); + expect(areDeepEqual(null, null)).toEqual(true); + }); + + it('can compare arrays', () => { + expect(areDeepEqual([], [])).toEqual(true); + expect(areDeepEqual([], undefined)).toEqual(false); + expect(areDeepEqual([], null)).toEqual(false); + expect(areDeepEqual(undefined, [])).toEqual(false); + expect(areDeepEqual(null, [])).toEqual(false); + + expect(areDeepEqual([1], [1])).toEqual(true); + expect(areDeepEqual([1], [2])).toEqual(false); + + expect(areDeepEqual([1, 2], [1, 2])).toEqual(true); + expect(areDeepEqual([1, 2], [2, 1])).toEqual(false); + + expect(areDeepEqual([1, 2, 3], [1, 2, 3])).toEqual(true); + expect(areDeepEqual([1, 2, 3], [1, 2, 4])).toEqual(false); + }); + + it('can compare objects', () => { + expect(areDeepEqual({}, {})).toEqual(true); + expect(areDeepEqual({}, undefined)).toEqual(false); + expect(areDeepEqual({}, null)).toEqual(false); + expect(areDeepEqual(undefined, {})).toEqual(false); + expect(areDeepEqual(null, {})).toEqual(false); + + expect(areDeepEqual({ a: 1 }, { a: 1 })).toEqual(true); + expect(areDeepEqual({ a: 1 }, { a: 2 })).toEqual(false); + expect(areDeepEqual({ a: 1 }, {})).toEqual(false); + expect(areDeepEqual({}, { a: 1 })).toEqual(false); + expect(areDeepEqual({ a: 1 }, { b: 1 })).toEqual(false); + + expect(areDeepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toEqual(true); + expect(areDeepEqual({ a: 1, b: 2 }, { a: 1, b: 3 })).toEqual(false); + expect(areDeepEqual({ a: 1, b: 2 }, { a: 1, c: 2 })).toEqual(false); + expect(areDeepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toEqual(true); + }); + + it('can compare nested objects', () => { + expect(areDeepEqual({ a: { b: 1 } }, { a: { b: 1 } })).toEqual(true); + expect(areDeepEqual({ a: { b: 1 } }, { a: { b: 2 } })).toEqual(false); + expect(areDeepEqual({ a: { b: 1 } }, { a: { c: 1 } })).toEqual(false); + expect(areDeepEqual({ a: { b: 1 } }, { a: { b: 1, c: 2 } })).toEqual(false); + expect(areDeepEqual({ a: { b: 1 } }, { a: { b: 1 }, c: 2 })).toEqual(false); + }); +}); diff --git a/libraries/node-core-library/src/user/getHomeFolder.ts b/libraries/node-core-library/src/user/getHomeFolder.ts new file mode 100644 index 00000000000..50fc90e0baf --- /dev/null +++ b/libraries/node-core-library/src/user/getHomeFolder.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; + +import { FileSystem } from '../FileSystem'; + +/** + * Returns the current user's home folder path. + * Throws if it cannot be determined. + * @public + */ +export function getHomeFolder(): string { + const unresolvedUserFolder: string | undefined = + process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME']; + const dirError: string = "Unable to determine the current user's home directory"; + if (unresolvedUserFolder === undefined) { + throw new Error(dirError); + } + + const homeFolder: string = path.resolve(unresolvedUserFolder); + if (!FileSystem.exists(homeFolder)) { + throw new Error(dirError); + } + + return homeFolder; +} diff --git a/libraries/node-core-library/src/user/index.ts b/libraries/node-core-library/src/user/index.ts new file mode 100644 index 00000000000..9e4ef36360e --- /dev/null +++ b/libraries/node-core-library/src/user/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export { getHomeFolder } from './getHomeFolder'; diff --git a/libraries/rush-lib/src/api/FlagFile.ts b/libraries/rush-lib/src/api/FlagFile.ts index 13466552817..535029943c9 100644 --- a/libraries/rush-lib/src/api/FlagFile.ts +++ b/libraries/rush-lib/src/api/FlagFile.ts @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { FileSystem, JsonFile, type JsonObject } from '@rushstack/node-core-library'; - -import { objectsAreDeepEqual } from '../utilities/objectUtilities'; +import { FileSystem, JsonFile, type JsonObject, Objects } from '@rushstack/node-core-library'; /** * A base class for flag file. @@ -38,7 +36,7 @@ export class FlagFile { try { oldState = await JsonFile.loadAsync(this.path); const newState: JsonObject = this._state; - return objectsAreDeepEqual(oldState, newState); + return Objects.areDeepEqual(oldState, newState); } catch (err) { return false; } diff --git a/libraries/rush-lib/src/api/LastInstallFlag.ts b/libraries/rush-lib/src/api/LastInstallFlag.ts index a20688cca32..ab854ac6ef7 100644 --- a/libraries/rush-lib/src/api/LastInstallFlag.ts +++ b/libraries/rush-lib/src/api/LastInstallFlag.ts @@ -3,7 +3,7 @@ import { pnpmSyncGetJsonVersion } from 'pnpm-sync-lib'; -import { JsonFile, type JsonObject, Path, type IPackageJson } from '@rushstack/node-core-library'; +import { JsonFile, type JsonObject, Path, type IPackageJson, Objects } from '@rushstack/node-core-library'; import type { PackageManagerName } from './packageManager/PackageManager'; import type { RushConfiguration } from './RushConfiguration'; @@ -124,7 +124,7 @@ export class LastInstallFlag extends FlagFile> { } } - if (!objectUtilities.objectsAreDeepEqual(oldState, newState)) { + if (!Objects.areDeepEqual(oldState, newState)) { if (checkValidAndReportStoreIssues) { const pkgManager: PackageManagerName = newState.packageManager as PackageManagerName; if (pkgManager === 'pnpm') { diff --git a/libraries/rush-lib/src/api/RushGlobalFolder.ts b/libraries/rush-lib/src/api/RushGlobalFolder.ts index e1081f2d4f7..18f77f7f938 100644 --- a/libraries/rush-lib/src/api/RushGlobalFolder.ts +++ b/libraries/rush-lib/src/api/RushGlobalFolder.ts @@ -3,7 +3,8 @@ import * as path from 'node:path'; -import { Utilities } from '../utilities/Utilities'; +import { User } from '@rushstack/node-core-library'; + import { EnvironmentConfiguration } from './EnvironmentConfiguration'; /** @@ -45,7 +46,7 @@ export class RushGlobalFolder { if (rushGlobalFolderOverride !== undefined) { this.path = rushGlobalFolderOverride; } else { - this.path = path.join(Utilities.getHomeFolder(), '.rush'); + this.path = path.join(User.getHomeFolder(), '.rush'); } const normalizedNodeVersion: string = process.version.match(/^[a-z0-9\-\.]+$/i) diff --git a/libraries/rush-lib/src/api/RushUserConfiguration.ts b/libraries/rush-lib/src/api/RushUserConfiguration.ts index 6b20ace19a3..cdf767363bb 100644 --- a/libraries/rush-lib/src/api/RushUserConfiguration.ts +++ b/libraries/rush-lib/src/api/RushUserConfiguration.ts @@ -3,9 +3,8 @@ import * as path from 'node:path'; -import { FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library'; +import { FileSystem, JsonFile, JsonSchema, User } from '@rushstack/node-core-library'; -import { Utilities } from '../utilities/Utilities'; import { RushConstants } from '../logic/RushConstants'; import schemaJson from '../schemas/rush-user-settings.schema.json'; @@ -52,7 +51,7 @@ export class RushUserConfiguration { } public static getRushUserFolderPath(): string { - const homeFolderPath: string = Utilities.getHomeFolder(); + const homeFolderPath: string = User.getHomeFolder(); return `${homeFolderPath}/${RushConstants.rushUserConfigurationFolderName}`; } } diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index 1b5a36318fe..7c7787a4362 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -9,7 +9,8 @@ import { FileConstants, FileSystem, JsonFile, - type JsonObject + type JsonObject, + Objects } from '@rushstack/node-core-library'; import { Colorize, @@ -28,7 +29,6 @@ import { PurgeManager } from '../logic/PurgeManager'; import type { IBuiltInPluginConfiguration } from '../pluginFramework/PluginLoader/BuiltInPluginLoader'; import type { BaseInstallManager } from '../logic/base/BaseInstallManager'; import type { IInstallManagerOptions } from '../logic/base/BaseInstallManagerTypes'; -import { objectsAreDeepEqual } from '../utilities/objectUtilities'; import { Utilities } from '../utilities/Utilities'; import type { Subspace } from '../api/Subspace'; import type { PnpmOptionsConfiguration } from '../logic/pnpm/PnpmOptionsConfiguration'; @@ -495,7 +495,7 @@ export class RushPnpmCommandLineParser { const currentGlobalPatchedDependencies: Record | undefined = pnpmOptions?.globalPatchedDependencies; - if (!objectsAreDeepEqual(currentGlobalPatchedDependencies, newGlobalPatchedDependencies)) { + if (!Objects.areDeepEqual(currentGlobalPatchedDependencies, newGlobalPatchedDependencies)) { const commonTempPnpmPatchesFolder: string = `${subspaceTempFolder}/${RushConstants.pnpmPatchesFolderName}`; const rushPnpmPatchesFolder: string = this._subspace.getSubspacePnpmPatchesFolderPath(); diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts index 0e52c9ad52c..30f86c1985e 100644 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ b/libraries/rush-lib/src/logic/CredentialCache.ts @@ -1,209 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'node:path'; - -import { FileSystem, JsonFile, JsonSchema, LockFile } from '@rushstack/node-core-library'; - -import { Utilities } from '../utilities/Utilities'; -import { RushUserConfiguration } from '../api/RushUserConfiguration'; -import schemaJson from '../schemas/credentials.schema.json'; -import { objectsAreDeepEqual } from '../utilities/objectUtilities'; - -const DEFAULT_CACHE_FILENAME: 'credentials.json' = 'credentials.json'; -const LATEST_CREDENTIALS_JSON_VERSION: string = '0.1.0'; - -interface ICredentialCacheJson { - version: string; - cacheEntries: { - [credentialCacheId: string]: ICacheEntryJson; - }; -} - -interface ICacheEntryJson { - expires: number; - credential: string; - credentialMetadata?: object; -} - -/** - * @beta - */ -export interface ICredentialCacheEntry { - expires?: Date; - credential: string; - credentialMetadata?: object; -} - -/** - * @beta - */ -export interface ICredentialCacheOptions { - supportEditing: boolean; - /** - * If specified, use the specified path instead of the default path of `~/.rush-user/credentials.json` - */ - cacheFilePath?: string; -} - -/** - * @beta - */ -export class CredentialCache /* implements IDisposable */ { - private readonly _cacheFilePath: string; - private readonly _cacheEntries: Map; - private _modified: boolean = false; - private _disposed: boolean = false; - private _supportsEditing: boolean; - private readonly _lockfile: LockFile | undefined; - - private constructor( - cacheFilePath: string, - loadedJson: ICredentialCacheJson | undefined, - lockfile: LockFile | undefined - ) { - if (loadedJson && loadedJson.version !== LATEST_CREDENTIALS_JSON_VERSION) { - throw new Error(`Unexpected ${cacheFilePath} file version: ${loadedJson.version}`); - } - - this._cacheFilePath = cacheFilePath; - this._cacheEntries = new Map(Object.entries(loadedJson?.cacheEntries || {})); - this._supportsEditing = !!lockfile; - this._lockfile = lockfile; - } - - public static async initializeAsync(options: ICredentialCacheOptions): Promise { - let cacheDirectory: string; - let cacheFileName: string; - if (options.cacheFilePath) { - cacheDirectory = path.dirname(options.cacheFilePath); - cacheFileName = options.cacheFilePath.slice(cacheDirectory.length + 1); - } else { - cacheDirectory = RushUserConfiguration.getRushUserFolderPath(); - cacheFileName = DEFAULT_CACHE_FILENAME; - } - const cacheFilePath: string = `${cacheDirectory}/${cacheFileName}`; - - const jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson); - - let loadedJson: ICredentialCacheJson | undefined; - try { - loadedJson = await JsonFile.loadAndValidateAsync(cacheFilePath, jsonSchema); - } catch (e) { - if (!FileSystem.isErrnoException(e as Error)) { - throw e; - } - } - - let lockfile: LockFile | undefined; - if (options.supportEditing) { - lockfile = await LockFile.acquireAsync(cacheDirectory, `${cacheFileName}.lock`); - } - - const credentialCache: CredentialCache = new CredentialCache(cacheFilePath, loadedJson, lockfile); - return credentialCache; - } - - public static async usingAsync( - options: ICredentialCacheOptions, - doActionAsync: (credentialCache: CredentialCache) => Promise | void - ): Promise { - await Utilities.usingAsync(async () => await CredentialCache.initializeAsync(options), doActionAsync); - } - - public setCacheEntry(cacheId: string, entry: ICredentialCacheEntry): void { - this._validate(true); - - const { expires, credential, credentialMetadata } = entry; - const expiresMilliseconds: number = expires?.getTime() || 0; - const existingCacheEntry: ICacheEntryJson | undefined = this._cacheEntries.get(cacheId); - if ( - existingCacheEntry?.credential !== credential || - existingCacheEntry?.expires !== expiresMilliseconds || - !objectsAreDeepEqual(existingCacheEntry?.credentialMetadata, credentialMetadata) - ) { - this._modified = true; - this._cacheEntries.set(cacheId, { - expires: expiresMilliseconds, - credential, - credentialMetadata - }); - } - } - - public tryGetCacheEntry(cacheId: string): ICredentialCacheEntry | undefined { - this._validate(false); - - const cacheEntry: ICacheEntryJson | undefined = this._cacheEntries.get(cacheId); - if (cacheEntry) { - const result: ICredentialCacheEntry = { - expires: cacheEntry.expires ? new Date(cacheEntry.expires) : undefined, - credential: cacheEntry.credential, - credentialMetadata: cacheEntry.credentialMetadata - }; - - return result; - } else { - return undefined; - } - } - - public deleteCacheEntry(cacheId: string): void { - this._validate(true); - - if (this._cacheEntries.has(cacheId)) { - this._modified = true; - this._cacheEntries.delete(cacheId); - } - } - - public trimExpiredEntries(): void { - this._validate(true); - - const now: number = Date.now(); - for (const [cacheId, cacheEntry] of this._cacheEntries.entries()) { - if (cacheEntry.expires < now) { - this._cacheEntries.delete(cacheId); - this._modified = true; - } - } - } - - public async saveIfModifiedAsync(): Promise { - this._validate(true); - - if (this._modified) { - const cacheEntriesJson: { [cacheId: string]: ICacheEntryJson } = {}; - for (const [cacheId, cacheEntry] of this._cacheEntries.entries()) { - cacheEntriesJson[cacheId] = cacheEntry; - } - - const newJson: ICredentialCacheJson = { - version: LATEST_CREDENTIALS_JSON_VERSION, - cacheEntries: cacheEntriesJson - }; - await JsonFile.saveAsync(newJson, this._cacheFilePath, { - ensureFolderExists: true, - updateExistingFile: true, - ignoreUndefinedValues: true - }); - - this._modified = false; - } - } - - public dispose(): void { - this._lockfile?.release(); - this._disposed = true; - } - - private _validate(requiresEditing: boolean): void { - if (!this._supportsEditing && requiresEditing) { - throw new Error(`This instance of ${CredentialCache.name} does not support editing.`); - } - - if (this._disposed) { - throw new Error(`This instance of ${CredentialCache.name} has been disposed.`); - } - } -} +export { + CredentialCache, + type ICredentialCacheEntry, + type ICredentialCacheOptions +} from '@rushstack/credential-cache'; diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 8343b6c8fc9..9411c549e43 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -13,6 +13,7 @@ import { AlreadyReportedError, Async, type IDependenciesMetaTable, + Objects, Path, Sort } from '@rushstack/node-core-library'; @@ -39,7 +40,6 @@ import { ShrinkwrapFileFactory } from '../ShrinkwrapFileFactory'; import { BaseProjectShrinkwrapFile } from '../base/BaseProjectShrinkwrapFile'; import { type CustomTipId, type ICustomTipInfo, PNPM_CUSTOM_TIPS } from '../../api/CustomTipsConfiguration'; import type { PnpmShrinkwrapFile } from '../pnpm/PnpmShrinkwrapFile'; -import { objectsAreDeepEqual } from '../../utilities/objectUtilities'; import type { Subspace } from '../../api/Subspace'; import { BaseLinkManager, SymlinkKind } from '../base/BaseLinkManager'; import { FlagFile } from '../../api/FlagFile'; @@ -372,7 +372,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { } // Now, we compare these two objects to see if they are equal or not - const dependenciesMetaAreEqual: boolean = objectsAreDeepEqual( + const dependenciesMetaAreEqual: boolean = Objects.areDeepEqual( expectedDependenciesMetaByProjectRelativePath, lockfileDependenciesMetaByProjectRelativePath ); @@ -388,7 +388,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { const pnpmOptions: PnpmOptionsConfiguration = subspace.getPnpmOptions() || this.rushConfiguration.pnpmOptions; - const overridesAreEqual: boolean = objectsAreDeepEqual>( + const overridesAreEqual: boolean = Objects.areDeepEqual>( pnpmOptions.globalOverrides ?? {}, shrinkwrapFile?.overrides ? Object.fromEntries(shrinkwrapFile?.overrides) : {} ); diff --git a/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts b/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts index 3a681a5b75e..ffbabe9e18b 100644 --- a/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts +++ b/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts @@ -11,7 +11,8 @@ import { InternalError, type JsonObject, NewlineKind, - Text + Text, + User } from '@rushstack/node-core-library'; import { PrintUtilities, Colorize, ConsoleTerminalProvider, Terminal } from '@rushstack/terminal'; @@ -353,7 +354,7 @@ export class SetupPackageRegistry { // ...then append the stuff we got from the REST API, but discard any junk that isn't a proper key/value linesToAdd.push(...responseLines.filter((x) => SetupPackageRegistry._getNpmrcKey(x) !== undefined)); - const npmrcPath: string = path.join(Utilities.getHomeFolder(), '.npmrc'); + const npmrcPath: string = path.join(User.getHomeFolder(), '.npmrc'); this._mergeLinesIntoNpmrc(npmrcPath, linesToAdd); } diff --git a/libraries/rush-lib/src/utilities/objectUtilities.ts b/libraries/rush-lib/src/utilities/objectUtilities.ts index 6224b94dc30..e282e6d6659 100644 --- a/libraries/rush-lib/src/utilities/objectUtilities.ts +++ b/libraries/rush-lib/src/utilities/objectUtilities.ts @@ -1,64 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { Objects } from '@rushstack/node-core-library'; + /** * Determines if two objects are deeply equal. */ -export function objectsAreDeepEqual(a: TObject, b: TObject): boolean { - if (a === b) { - return true; - } else { - const aType: string = typeof a; - const bType: string = typeof b; - if (aType !== bType) { - return false; - } else { - if (aType === 'object') { - if (a === null || b === null) { - // We already handled the case where a === b, so if either is null, they are not equal - return false; - } else if (Array.isArray(a)) { - if (!Array.isArray(b) || a.length !== b.length) { - return false; - } else { - for (let i: number = 0; i < a.length; ++i) { - if (!objectsAreDeepEqual(a[i], b[i])) { - return false; - } - } - - return true; - } - } else { - const aObjectProperties: Set = new Set(Object.getOwnPropertyNames(a)); - const bObjectProperties: Set = new Set(Object.getOwnPropertyNames(b)); - if (aObjectProperties.size !== bObjectProperties.size) { - return false; - } else { - for (const property of aObjectProperties) { - if (bObjectProperties.delete(property)) { - if ( - !objectsAreDeepEqual( - (a as Record)[property], - (b as Record)[property] - ) - ) { - return false; - } - } else { - return false; - } - } - - return bObjectProperties.size === 0; - } - } - } else { - return false; - } - } - } -} +export const objectsAreDeepEqual: typeof Objects.areDeepEqual = Objects.areDeepEqual; export function cloneDeep(obj: TObject): TObject { return cloneDeepInner(obj, new Set()); diff --git a/libraries/rush-lib/src/utilities/test/objectUtilities.test.ts b/libraries/rush-lib/src/utilities/test/objectUtilities.test.ts index 9235fd63ab8..ce023119369 100644 --- a/libraries/rush-lib/src/utilities/test/objectUtilities.test.ts +++ b/libraries/rush-lib/src/utilities/test/objectUtilities.test.ts @@ -1,82 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { objectsAreDeepEqual, cloneDeep, merge, removeNullishProps } from '../objectUtilities'; +import { cloneDeep, merge, removeNullishProps } from '../objectUtilities'; describe('objectUtilities', () => { - describe(objectsAreDeepEqual.name, () => { - it('can compare primitives', () => { - expect(objectsAreDeepEqual(1, 1)).toEqual(true); - expect(objectsAreDeepEqual(1, undefined)).toEqual(false); - expect(objectsAreDeepEqual(1, null)).toEqual(false); - expect(objectsAreDeepEqual(undefined, 1)).toEqual(false); - expect(objectsAreDeepEqual(null, 1)).toEqual(false); - expect(objectsAreDeepEqual(1, 2)).toEqual(false); - - expect(objectsAreDeepEqual('a', 'a')).toEqual(true); - expect(objectsAreDeepEqual('a', undefined)).toEqual(false); - expect(objectsAreDeepEqual('a', null)).toEqual(false); - expect(objectsAreDeepEqual(undefined, 'a')).toEqual(false); - expect(objectsAreDeepEqual(null, 'a')).toEqual(false); - expect(objectsAreDeepEqual('a', 'b')).toEqual(false); - - expect(objectsAreDeepEqual(true, true)).toEqual(true); - expect(objectsAreDeepEqual(true, undefined)).toEqual(false); - expect(objectsAreDeepEqual(true, null)).toEqual(false); - expect(objectsAreDeepEqual(undefined, true)).toEqual(false); - expect(objectsAreDeepEqual(null, true)).toEqual(false); - expect(objectsAreDeepEqual(true, false)).toEqual(false); - - expect(objectsAreDeepEqual(undefined, undefined)).toEqual(true); - expect(objectsAreDeepEqual(undefined, null)).toEqual(false); - expect(objectsAreDeepEqual(null, null)).toEqual(true); - }); - - it('can compare arrays', () => { - expect(objectsAreDeepEqual([], [])).toEqual(true); - expect(objectsAreDeepEqual([], undefined)).toEqual(false); - expect(objectsAreDeepEqual([], null)).toEqual(false); - expect(objectsAreDeepEqual(undefined, [])).toEqual(false); - expect(objectsAreDeepEqual(null, [])).toEqual(false); - - expect(objectsAreDeepEqual([1], [1])).toEqual(true); - expect(objectsAreDeepEqual([1], [2])).toEqual(false); - - expect(objectsAreDeepEqual([1, 2], [1, 2])).toEqual(true); - expect(objectsAreDeepEqual([1, 2], [2, 1])).toEqual(false); - - expect(objectsAreDeepEqual([1, 2, 3], [1, 2, 3])).toEqual(true); - expect(objectsAreDeepEqual([1, 2, 3], [1, 2, 4])).toEqual(false); - }); - - it('can compare objects', () => { - expect(objectsAreDeepEqual({}, {})).toEqual(true); - expect(objectsAreDeepEqual({}, undefined)).toEqual(false); - expect(objectsAreDeepEqual({}, null)).toEqual(false); - expect(objectsAreDeepEqual(undefined, {})).toEqual(false); - expect(objectsAreDeepEqual(null, {})).toEqual(false); - - expect(objectsAreDeepEqual({ a: 1 }, { a: 1 })).toEqual(true); - expect(objectsAreDeepEqual({ a: 1 }, { a: 2 })).toEqual(false); - expect(objectsAreDeepEqual({ a: 1 }, {})).toEqual(false); - expect(objectsAreDeepEqual({}, { a: 1 })).toEqual(false); - expect(objectsAreDeepEqual({ a: 1 }, { b: 1 })).toEqual(false); - - expect(objectsAreDeepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toEqual(true); - expect(objectsAreDeepEqual({ a: 1, b: 2 }, { a: 1, b: 3 })).toEqual(false); - expect(objectsAreDeepEqual({ a: 1, b: 2 }, { a: 1, c: 2 })).toEqual(false); - expect(objectsAreDeepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toEqual(true); - }); - - it('can compare nested objects', () => { - expect(objectsAreDeepEqual({ a: { b: 1 } }, { a: { b: 1 } })).toEqual(true); - expect(objectsAreDeepEqual({ a: { b: 1 } }, { a: { b: 2 } })).toEqual(false); - expect(objectsAreDeepEqual({ a: { b: 1 } }, { a: { c: 1 } })).toEqual(false); - expect(objectsAreDeepEqual({ a: { b: 1 } }, { a: { b: 1, c: 2 } })).toEqual(false); - expect(objectsAreDeepEqual({ a: { b: 1 } }, { a: { b: 1 }, c: 2 })).toEqual(false); - }); - }); - describe(cloneDeep.name, () => { function testClone(source: unknown): void { const clone: unknown = cloneDeep(source); diff --git a/libraries/rush-sdk/package.json b/libraries/rush-sdk/package.json index a47186ad257..5ee83a57f4f 100644 --- a/libraries/rush-sdk/package.json +++ b/libraries/rush-sdk/package.json @@ -40,6 +40,7 @@ "license": "MIT", "dependencies": { "@pnpm/lockfile.types": "~1.0.3", + "@rushstack/credential-cache": "workspace:*", "@rushstack/lookup-by-path": "workspace:*", "@rushstack/node-core-library": "workspace:*", "@rushstack/package-deps-hash": "workspace:*", diff --git a/rush-plugins/rush-amazon-s3-build-cache-plugin/package.json b/rush-plugins/rush-amazon-s3-build-cache-plugin/package.json index 970c2756992..8ae7cfb51ce 100644 --- a/rush-plugins/rush-amazon-s3-build-cache-plugin/package.json +++ b/rush-plugins/rush-amazon-s3-build-cache-plugin/package.json @@ -19,6 +19,7 @@ "_phase:test": "heft run --only test -- --clean" }, "dependencies": { + "@rushstack/credential-cache": "workspace:*", "@rushstack/node-core-library": "workspace:*", "@rushstack/rush-sdk": "workspace:*", "@rushstack/terminal": "workspace:*", diff --git a/rush-plugins/rush-amazon-s3-build-cache-plugin/src/AmazonS3BuildCacheProvider.ts b/rush-plugins/rush-amazon-s3-build-cache-plugin/src/AmazonS3BuildCacheProvider.ts index 2d4292bd8af..ea8d7b903c0 100644 --- a/rush-plugins/rush-amazon-s3-build-cache-plugin/src/AmazonS3BuildCacheProvider.ts +++ b/rush-plugins/rush-amazon-s3-build-cache-plugin/src/AmazonS3BuildCacheProvider.ts @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { type ICredentialCacheEntry, CredentialCache } from '@rushstack/credential-cache'; import type { ITerminal } from '@rushstack/terminal'; import { type ICloudBuildCacheProvider, - type ICredentialCacheEntry, - CredentialCache, type RushSession, RushConstants, EnvironmentVariableNames, diff --git a/rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/AmazonS3BuildCacheProvider.test.ts b/rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/AmazonS3BuildCacheProvider.test.ts index f66b89415ed..f846371ff0b 100644 --- a/rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/AmazonS3BuildCacheProvider.test.ts +++ b/rush-plugins/rush-amazon-s3-build-cache-plugin/src/test/AmazonS3BuildCacheProvider.test.ts @@ -5,13 +5,9 @@ jest.mock('@rushstack/rush-sdk/lib/utilities/WebClient', () => { return jest.requireActual('@microsoft/rush-lib/lib/utilities/WebClient'); }); +import { CredentialCache } from '@rushstack/credential-cache'; import { ConsoleTerminalProvider, StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; -import { - RushSession, - CredentialCache, - EnvironmentConfiguration, - RushUserConfiguration -} from '@rushstack/rush-sdk'; +import { RushSession, EnvironmentConfiguration, RushUserConfiguration } from '@rushstack/rush-sdk'; import { AmazonS3BuildCacheProvider } from '../AmazonS3BuildCacheProvider'; diff --git a/rush-plugins/rush-azure-storage-build-cache-plugin/package.json b/rush-plugins/rush-azure-storage-build-cache-plugin/package.json index 99d2feffe64..c326b6f4467 100644 --- a/rush-plugins/rush-azure-storage-build-cache-plugin/package.json +++ b/rush-plugins/rush-azure-storage-build-cache-plugin/package.json @@ -20,6 +20,7 @@ "dependencies": { "@azure/identity": "~4.5.0", "@azure/storage-blob": "~12.26.0", + "@rushstack/credential-cache": "workspace:*", "@rushstack/node-core-library": "workspace:*", "@rushstack/rush-sdk": "workspace:*", "@rushstack/terminal": "workspace:*" diff --git a/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureAuthenticationBase.ts b/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureAuthenticationBase.ts index f5c968e1328..2be93baa713 100644 --- a/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureAuthenticationBase.ts +++ b/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureAuthenticationBase.ts @@ -19,10 +19,10 @@ import { } from '@azure/identity'; import type { ITerminal } from '@rushstack/terminal'; -import { CredentialCache } from '@rushstack/rush-sdk'; +import { CredentialCache } from '@rushstack/credential-cache'; // Use a separate import line so the .d.ts file ends up with an `import type { ... }` // See https://github.com/microsoft/rushstack/issues/3432 -import type { ICredentialCacheEntry } from '@rushstack/rush-sdk'; +import type { ICredentialCacheEntry } from '@rushstack/credential-cache'; import { PrintUtilities } from '@rushstack/terminal'; import { AdoCodespacesAuthCredential } from './AdoCodespacesAuthCredential'; diff --git a/rush-plugins/rush-azure-storage-build-cache-plugin/src/test/AzureStorageBuildCacheProvider.test.ts b/rush-plugins/rush-azure-storage-build-cache-plugin/src/test/AzureStorageBuildCacheProvider.test.ts index 4bde79dd705..23bb0ead55b 100644 --- a/rush-plugins/rush-azure-storage-build-cache-plugin/src/test/AzureStorageBuildCacheProvider.test.ts +++ b/rush-plugins/rush-azure-storage-build-cache-plugin/src/test/AzureStorageBuildCacheProvider.test.ts @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { CredentialCache } from '@rushstack/credential-cache'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; -import { CredentialCache, EnvironmentConfiguration, RushUserConfiguration } from '@rushstack/rush-sdk'; +import { EnvironmentConfiguration, RushUserConfiguration } from '@rushstack/rush-sdk'; import { AzureStorageBuildCacheProvider } from '../AzureStorageBuildCacheProvider'; import type { AzureEnvironmentName } from '../AzureAuthenticationBase'; diff --git a/rush-plugins/rush-http-build-cache-plugin/package.json b/rush-plugins/rush-http-build-cache-plugin/package.json index 369e18c3ffd..0e85ee46bb1 100644 --- a/rush-plugins/rush-http-build-cache-plugin/package.json +++ b/rush-plugins/rush-http-build-cache-plugin/package.json @@ -19,6 +19,7 @@ "_phase:test": "heft run --only test -- --clean" }, "dependencies": { + "@rushstack/credential-cache": "workspace:*", "@rushstack/node-core-library": "workspace:*", "@rushstack/rush-sdk": "workspace:*", "https-proxy-agent": "~5.0.0" diff --git a/rush-plugins/rush-http-build-cache-plugin/src/HttpBuildCacheProvider.ts b/rush-plugins/rush-http-build-cache-plugin/src/HttpBuildCacheProvider.ts index 8868fdbd09f..5acdd8a9457 100644 --- a/rush-plugins/rush-http-build-cache-plugin/src/HttpBuildCacheProvider.ts +++ b/rush-plugins/rush-http-build-cache-plugin/src/HttpBuildCacheProvider.ts @@ -3,12 +3,11 @@ import type { SpawnSyncReturns } from 'node:child_process'; +import { type ICredentialCacheEntry, CredentialCache } from '@rushstack/credential-cache'; import { Executable, Async } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; import { type ICloudBuildCacheProvider, - type ICredentialCacheEntry, - CredentialCache, type RushSession, EnvironmentConfiguration } from '@rushstack/rush-sdk'; From 5dc4d8da73891abf410667319b0ed2c1f867d4e2 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 02:07:07 +0000 Subject: [PATCH 04/14] Fix routing --- libraries/rush-lib/src/index.ts | 15 ++++++++------- libraries/rush-lib/src/logic/CredentialCache.ts | 8 -------- 2 files changed, 8 insertions(+), 15 deletions(-) delete mode 100644 libraries/rush-lib/src/logic/CredentialCache.ts diff --git a/libraries/rush-lib/src/index.ts b/libraries/rush-lib/src/index.ts index 85a59f9ad77..88dfb89789e 100644 --- a/libraries/rush-lib/src/index.ts +++ b/libraries/rush-lib/src/index.ts @@ -8,9 +8,16 @@ * @packageDocumentation */ -// For backwards compatibility +// #region Backwards compatibility export { LookupByPath as LookupByPath, type IPrefixMatch } from '@rushstack/lookup-by-path'; +export { + type ICredentialCacheOptions, + type ICredentialCacheEntry, + CredentialCache +} from '@rushstack/credential-cache'; +// #endregion + export { ApprovedPackagesPolicy } from './api/ApprovedPackagesPolicy'; export { RushConfiguration, type ITryFindRushJsonLocationOptions } from './api/RushConfiguration'; @@ -176,12 +183,6 @@ export type { ICobuildCompletedState } from './logic/cobuild/ICobuildLockProvider'; -export { - type ICredentialCacheOptions, - type ICredentialCacheEntry, - CredentialCache -} from './logic/CredentialCache'; - export type { ITelemetryData, ITelemetryMachineInfo, ITelemetryOperationResult } from './logic/Telemetry'; export type { IStopwatchResult } from './utilities/Stopwatch'; diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts deleted file mode 100644 index 30f86c1985e..00000000000 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -export { - CredentialCache, - type ICredentialCacheEntry, - type ICredentialCacheOptions -} from '@rushstack/credential-cache'; From aa5d6457815c3970d9446d4ab0d92efa86f777d1 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 02:09:28 +0000 Subject: [PATCH 05/14] Fix API --- .../reviews/api/rush-azure-storage-build-cache-plugin.api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/reviews/api/rush-azure-storage-build-cache-plugin.api.md b/common/reviews/api/rush-azure-storage-build-cache-plugin.api.md index 4bd893d8be2..26f39c4993a 100644 --- a/common/reviews/api/rush-azure-storage-build-cache-plugin.api.md +++ b/common/reviews/api/rush-azure-storage-build-cache-plugin.api.md @@ -5,9 +5,9 @@ ```ts import { AzureAuthorityHosts } from '@azure/identity'; -import { CredentialCache } from '@rushstack/rush-sdk'; +import { CredentialCache } from '@rushstack/credential-cache'; import { DeviceCodeCredentialOptions } from '@azure/identity'; -import type { ICredentialCacheEntry } from '@rushstack/rush-sdk'; +import type { ICredentialCacheEntry } from '@rushstack/credential-cache'; import { InteractiveBrowserCredentialNodeOptions } from '@azure/identity'; import type { IRushPlugin } from '@rushstack/rush-sdk'; import type { ITerminal } from '@rushstack/terminal'; From ca34e0942bce97a48048b5db90cd6042cd5b028c Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 02:13:02 +0000 Subject: [PATCH 06/14] Update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c75a4436ce1..5fbf77f7665 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/heft-plugins/heft-webpack4-plugin](./heft-plugins/heft-webpack4-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-webpack4-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-webpack4-plugin) | [changelog](./heft-plugins/heft-webpack4-plugin/CHANGELOG.md) | [@rushstack/heft-webpack4-plugin](https://www.npmjs.com/package/@rushstack/heft-webpack4-plugin) | | [/heft-plugins/heft-webpack5-plugin](./heft-plugins/heft-webpack5-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-webpack5-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-webpack5-plugin) | [changelog](./heft-plugins/heft-webpack5-plugin/CHANGELOG.md) | [@rushstack/heft-webpack5-plugin](https://www.npmjs.com/package/@rushstack/heft-webpack5-plugin) | | [/libraries/api-extractor-model](./libraries/api-extractor-model/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Fapi-extractor-model.svg)](https://badge.fury.io/js/%40microsoft%2Fapi-extractor-model) | [changelog](./libraries/api-extractor-model/CHANGELOG.md) | [@microsoft/api-extractor-model](https://www.npmjs.com/package/@microsoft/api-extractor-model) | +| [/libraries/credential-cache](./libraries/credential-cache/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fcredential-cache.svg)](https://badge.fury.io/js/%40rushstack%2Fcredential-cache) | [changelog](./libraries/credential-cache/CHANGELOG.md) | [@rushstack/credential-cache](https://www.npmjs.com/package/@rushstack/credential-cache) | | [/libraries/debug-certificate-manager](./libraries/debug-certificate-manager/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fdebug-certificate-manager.svg)](https://badge.fury.io/js/%40rushstack%2Fdebug-certificate-manager) | [changelog](./libraries/debug-certificate-manager/CHANGELOG.md) | [@rushstack/debug-certificate-manager](https://www.npmjs.com/package/@rushstack/debug-certificate-manager) | | [/libraries/heft-config-file](./libraries/heft-config-file/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-config-file.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-config-file) | [changelog](./libraries/heft-config-file/CHANGELOG.md) | [@rushstack/heft-config-file](https://www.npmjs.com/package/@rushstack/heft-config-file) | | [/libraries/load-themed-styles](./libraries/load-themed-styles/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Fload-themed-styles.svg)](https://badge.fury.io/js/%40microsoft%2Fload-themed-styles) | [changelog](./libraries/load-themed-styles/CHANGELOG.md) | [@microsoft/load-themed-styles](https://www.npmjs.com/package/@microsoft/load-themed-styles) | From 06ae7542ab6eb3ba04a59beb4a558749df31b4cd Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 02:14:01 +0000 Subject: [PATCH 07/14] Update readme --- libraries/credential-cache/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/credential-cache/README.md b/libraries/credential-cache/README.md index 058aa227884..72a9d9393cb 100644 --- a/libraries/credential-cache/README.md +++ b/libraries/credential-cache/README.md @@ -6,6 +6,7 @@ ## Overview +This package manages persistent credentials on the local machine. Since these credentials are stored unencrypted, do not use for secure credentials. ## Links From eaed80df2d6e3a903e09ff99c9fc60556c0308f8 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 18:00:09 +0000 Subject: [PATCH 08/14] rush update --- .../build-tests-subspace/pnpm-lock.yaml | 35 ++++++++++++------- .../build-tests-subspace/repo-state.json | 4 +-- .../config/subspaces/default/pnpm-lock.yaml | 31 ++++++++++++++++ 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index 0092b867cf9..87c86fc3784 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -111,10 +111,10 @@ importers: version: file:../../../apps/heft(@types/node@20.17.19) '@rushstack/heft-lint-plugin': specifier: file:../../heft-plugins/heft-lint-plugin - version: file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19) + version: file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19) '@rushstack/heft-typescript-plugin': specifier: file:../../heft-plugins/heft-typescript-plugin - version: file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19) + version: file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19) eslint: specifier: ~9.25.1 version: 9.25.1 @@ -7842,7 +7842,7 @@ packages: - supports-color dev: true - file:../../../heft-plugins/heft-api-extractor-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19): + file:../../../heft-plugins/heft-api-extractor-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19): resolution: {directory: ../../../heft-plugins/heft-api-extractor-plugin, type: directory} id: file:../../../heft-plugins/heft-api-extractor-plugin name: '@rushstack/heft-api-extractor-plugin' @@ -7856,7 +7856,7 @@ packages: - '@types/node' dev: true - file:../../../heft-plugins/heft-jest-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19)(jest-environment-node@29.5.0): + file:../../../heft-plugins/heft-jest-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19)(jest-environment-node@29.5.0): resolution: {directory: ../../../heft-plugins/heft-jest-plugin, type: directory} id: file:../../../heft-plugins/heft-jest-plugin name: '@rushstack/heft-jest-plugin' @@ -7891,7 +7891,7 @@ packages: - ts-node dev: true - file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19): + file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19): resolution: {directory: ../../../heft-plugins/heft-lint-plugin, type: directory} id: file:../../../heft-plugins/heft-lint-plugin name: '@rushstack/heft-lint-plugin' @@ -7906,7 +7906,7 @@ packages: - '@types/node' dev: true - file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19): + file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19): resolution: {directory: ../../../heft-plugins/heft-typescript-plugin, type: directory} id: file:../../../heft-plugins/heft-typescript-plugin name: '@rushstack/heft-typescript-plugin' @@ -7935,6 +7935,15 @@ packages: - '@types/node' dev: true + file:../../../libraries/credential-cache(@types/node@20.17.19): + resolution: {directory: ../../../libraries/credential-cache, type: directory} + id: file:../../../libraries/credential-cache + name: '@rushstack/credential-cache' + dependencies: + '@rushstack/node-core-library': file:../../../libraries/node-core-library(@types/node@20.17.19) + transitivePeerDependencies: + - '@types/node' + file:../../../libraries/heft-config-file(@types/node@20.17.19): resolution: {directory: ../../../libraries/heft-config-file, type: directory} id: file:../../../libraries/heft-config-file @@ -8062,6 +8071,7 @@ packages: '@pnpm/dependency-path-lockfile-pre-v10': /@pnpm/dependency-path@5.1.7 '@pnpm/dependency-path-lockfile-pre-v9': /@pnpm/dependency-path@2.1.8 '@pnpm/link-bins': 5.3.25 + '@rushstack/credential-cache': file:../../../libraries/credential-cache(@types/node@20.17.19) '@rushstack/heft-config-file': file:../../../libraries/heft-config-file(@types/node@20.17.19) '@rushstack/lookup-by-path': file:../../../libraries/lookup-by-path(@types/node@20.17.19) '@rushstack/node-core-library': file:../../../libraries/node-core-library(@types/node@20.17.19) @@ -8109,6 +8119,7 @@ packages: name: '@rushstack/rush-sdk' dependencies: '@pnpm/lockfile.types': 1.0.3 + '@rushstack/credential-cache': file:../../../libraries/credential-cache(@types/node@20.17.19) '@rushstack/lookup-by-path': file:../../../libraries/lookup-by-path(@types/node@20.17.19) '@rushstack/node-core-library': file:../../../libraries/node-core-library(@types/node@20.17.19) '@rushstack/package-deps-hash': file:../../../libraries/package-deps-hash(@types/node@20.17.19) @@ -8160,7 +8171,7 @@ packages: transitivePeerDependencies: - '@types/node' - file:../../../rigs/heft-node-rig(@rushstack/heft@1.1.1)(@types/node@20.17.19): + file:../../../rigs/heft-node-rig(@rushstack/heft@1.1.2)(@types/node@20.17.19): resolution: {directory: ../../../rigs/heft-node-rig, type: directory} id: file:../../../rigs/heft-node-rig name: '@rushstack/heft-node-rig' @@ -8170,10 +8181,10 @@ packages: '@microsoft/api-extractor': file:../../../apps/api-extractor(@types/node@20.17.19) '@rushstack/eslint-config': file:../../../eslint/eslint-config(eslint@9.37.0)(typescript@5.8.2) '@rushstack/heft': file:../../../apps/heft(@types/node@20.17.19) - '@rushstack/heft-api-extractor-plugin': file:../../../heft-plugins/heft-api-extractor-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19) - '@rushstack/heft-jest-plugin': file:../../../heft-plugins/heft-jest-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19)(jest-environment-node@29.5.0) - '@rushstack/heft-lint-plugin': file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19) - '@rushstack/heft-typescript-plugin': file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@1.1.1)(@types/node@20.17.19) + '@rushstack/heft-api-extractor-plugin': file:../../../heft-plugins/heft-api-extractor-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19) + '@rushstack/heft-jest-plugin': file:../../../heft-plugins/heft-jest-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19)(jest-environment-node@29.5.0) + '@rushstack/heft-lint-plugin': file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19) + '@rushstack/heft-typescript-plugin': file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@1.1.2)(@types/node@20.17.19) '@types/heft-jest': 1.0.1 eslint: 9.37.0 jest-environment-node: 29.5.0 @@ -8195,7 +8206,7 @@ packages: '@microsoft/api-extractor': file:../../../apps/api-extractor(@types/node@20.17.19) '@rushstack/eslint-patch': file:../../../eslint/eslint-patch '@rushstack/heft': file:../../../apps/heft(@types/node@20.17.19) - '@rushstack/heft-node-rig': file:../../../rigs/heft-node-rig(@rushstack/heft@1.1.1)(@types/node@20.17.19) + '@rushstack/heft-node-rig': file:../../../rigs/heft-node-rig(@rushstack/heft@1.1.2)(@types/node@20.17.19) '@types/heft-jest': 1.0.1 '@types/node': 20.17.19 eslint: 9.37.0 diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index 1e915afede2..20cd48a9b6b 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -1,6 +1,6 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "1dc2b69423ca353582cec8716410a0b8a8210aff", + "pnpmShrinkwrapHash": "c9e998d8d98c587075f2380a11aa0816aecfd2fc", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "7bf222162008849ca931709cbaa90632d56766da" + "packageJsonInjectedDependenciesHash": "9c50330f156d5d74c5dd1fb2e3aa6b9c2a2403c7" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 9465e8c01d2..1c54bb7c298 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -3365,6 +3365,22 @@ importers: specifier: ~9.37.0 version: 9.37.0(supports-color@8.1.1) + ../../../libraries/credential-cache: + dependencies: + '@rushstack/node-core-library': + specifier: workspace:* + version: link:../node-core-library + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + eslint: + specifier: ~9.37.0 + version: 9.37.0(supports-color@8.1.1) + local-node-rig: + specifier: workspace:* + version: link:../../rigs/local-node-rig + ../../../libraries/debug-certificate-manager: dependencies: '@rushstack/node-core-library': @@ -3737,6 +3753,9 @@ importers: '@pnpm/link-bins': specifier: ~5.3.7 version: 5.3.25 + '@rushstack/credential-cache': + specifier: workspace:* + version: link:../credential-cache '@rushstack/heft-config-file': specifier: workspace:* version: link:../heft-config-file @@ -3909,6 +3928,9 @@ importers: '@pnpm/lockfile.types': specifier: ~1.0.3 version: 1.0.3 + '@rushstack/credential-cache': + specifier: workspace:* + version: link:../credential-cache '@rushstack/lookup-by-path': specifier: workspace:* version: link:../lookup-by-path @@ -4497,6 +4519,9 @@ importers: ../../../rush-plugins/rush-amazon-s3-build-cache-plugin: dependencies: + '@rushstack/credential-cache': + specifier: workspace:* + version: link:../../libraries/credential-cache '@rushstack/node-core-library': specifier: workspace:* version: link:../../libraries/node-core-library @@ -4531,6 +4556,9 @@ importers: '@azure/storage-blob': specifier: ~12.26.0 version: 12.26.0 + '@rushstack/credential-cache': + specifier: workspace:* + version: link:../../libraries/credential-cache '@rushstack/node-core-library': specifier: workspace:* version: link:../../libraries/node-core-library @@ -4609,6 +4637,9 @@ importers: ../../../rush-plugins/rush-http-build-cache-plugin: dependencies: + '@rushstack/credential-cache': + specifier: workspace:* + version: link:../../libraries/credential-cache '@rushstack/node-core-library': specifier: workspace:* version: link:../../libraries/node-core-library From ffad4c8bcc17677b42874aa524d9a90c261c55c4 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 22:36:47 +0000 Subject: [PATCH 09/14] Forward rush-user folder name --- libraries/credential-cache/src/CredentialCache.ts | 8 +++++++- libraries/credential-cache/src/index.ts | 7 ++++++- .../credential-cache/src/test/CredentialCache.test.ts | 4 ++-- libraries/rush-lib/src/logic/RushConstants.ts | 7 ++++++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/libraries/credential-cache/src/CredentialCache.ts b/libraries/credential-cache/src/CredentialCache.ts index 606f01c2a8f..68bf10da706 100644 --- a/libraries/credential-cache/src/CredentialCache.ts +++ b/libraries/credential-cache/src/CredentialCache.ts @@ -7,6 +7,12 @@ import { FileSystem, JsonFile, JsonSchema, LockFile, User, Objects } from '@rush import schemaJson from './schemas/credentials.schema.json'; +/** + * The name of the default folder in the user's home directory where Rush stores user-specific data. + * @public + */ +export const RUSH_USER_FOLDER_NAME: '.rush-user' = '.rush-user'; + const DEFAULT_CACHE_FILENAME: 'credentials.json' = 'credentials.json'; const LATEST_CREDENTIALS_JSON_VERSION: string = '0.1.0'; @@ -76,7 +82,7 @@ export class CredentialCache implements Disposable { cacheDirectory = path.dirname(options.cacheFilePath); cacheFileName = options.cacheFilePath.slice(cacheDirectory.length + 1); } else { - cacheDirectory = `${User.getHomeFolder()}/.rush-user`; + cacheDirectory = `${User.getHomeFolder()}/${RUSH_USER_FOLDER_NAME}`; cacheFileName = DEFAULT_CACHE_FILENAME; } const cacheFilePath: string = `${cacheDirectory}/${cacheFileName}`; diff --git a/libraries/credential-cache/src/index.ts b/libraries/credential-cache/src/index.ts index 6c5470d8ed1..00ce0172b53 100644 --- a/libraries/credential-cache/src/index.ts +++ b/libraries/credential-cache/src/index.ts @@ -7,4 +7,9 @@ * @packageDocumentation */ -export { CredentialCache, type ICredentialCacheEntry, type ICredentialCacheOptions } from './CredentialCache'; +export { + CredentialCache, + type ICredentialCacheEntry, + type ICredentialCacheOptions, + RUSH_USER_FOLDER_NAME +} from './CredentialCache'; diff --git a/libraries/credential-cache/src/test/CredentialCache.test.ts b/libraries/credential-cache/src/test/CredentialCache.test.ts index 29b96a1135c..5bdb13c49e6 100644 --- a/libraries/credential-cache/src/test/CredentialCache.test.ts +++ b/libraries/credential-cache/src/test/CredentialCache.test.ts @@ -3,10 +3,10 @@ import { mockGetHomeFolder } from './CredentialCache.mock'; import { LockFile, Async, FileSystem } from '@rushstack/node-core-library'; -import { CredentialCache, type ICredentialCacheOptions } from '../CredentialCache'; +import { CredentialCache, type ICredentialCacheOptions, RUSH_USER_FOLDER_NAME } from '../CredentialCache'; const FAKE_HOME_FOLDER: string = 'temp'; -const FAKE_RUSH_USER_FOLDER: string = `${FAKE_HOME_FOLDER}/.rush-user`; +const FAKE_RUSH_USER_FOLDER: string = `${FAKE_HOME_FOLDER}/${RUSH_USER_FOLDER_NAME}`; interface IPathsTestCase extends Required> { testCaseName: string; diff --git a/libraries/rush-lib/src/logic/RushConstants.ts b/libraries/rush-lib/src/logic/RushConstants.ts index e04c03d2f2e..0f5ed03c8e8 100644 --- a/libraries/rush-lib/src/logic/RushConstants.ts +++ b/libraries/rush-lib/src/logic/RushConstants.ts @@ -1,6 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import type { RUSH_USER_FOLDER_NAME } from '@rushstack/credential-cache'; + +// Use the typing here to enforce consistency between the two libraries +const rushUserConfigurationFolderName: typeof RUSH_USER_FOLDER_NAME = '.rush-user'; + /** * Constants used by the Rush tool. * @beta @@ -298,7 +303,7 @@ export class RushConstants { /** * The name of the per-user Rush configuration data folder. */ - public static readonly rushUserConfigurationFolderName: '.rush-user' = '.rush-user'; + public static readonly rushUserConfigurationFolderName: '.rush-user' = rushUserConfigurationFolderName; /** * The name of the project `rush-logs` folder. From 1bf994c79f2b6a826c10ac5fa6a3b72fe6e0d161 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 22:37:03 +0000 Subject: [PATCH 10/14] Remove old credentials schema --- .../src/schemas/credentials.schema.json | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 libraries/rush-lib/src/schemas/credentials.schema.json diff --git a/libraries/rush-lib/src/schemas/credentials.schema.json b/libraries/rush-lib/src/schemas/credentials.schema.json deleted file mode 100644 index 9800b511877..00000000000 --- a/libraries/rush-lib/src/schemas/credentials.schema.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Cache for credentials used with the Rush tool.", - "description": "For use with the Rush tool, this file acts as a cache for the credentials. See http://rushjs.io for details.", - - "type": "object", - - "required": ["version", "cacheEntries"], - "properties": { - "version": { - "type": "string" - }, - - "cacheEntries": { - "type": "object", - "patternProperties": { - ".+": { - "type": "object", - "required": ["expires", "credential"], - - "properties": { - "expires": { - "type": "number" - }, - "credential": { - "type": "string" - }, - "credentialMetadata": { - "type": "object" - } - } - } - } - } - } -} From dee98a99d5602cd991ae6d6c18fc49f8b0a0f741 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 22:39:24 +0000 Subject: [PATCH 11/14] Restore caching in getHomeFolder --- libraries/node-core-library/src/user/getHomeFolder.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libraries/node-core-library/src/user/getHomeFolder.ts b/libraries/node-core-library/src/user/getHomeFolder.ts index 50fc90e0baf..61db1b38714 100644 --- a/libraries/node-core-library/src/user/getHomeFolder.ts +++ b/libraries/node-core-library/src/user/getHomeFolder.ts @@ -5,12 +5,18 @@ import * as path from 'node:path'; import { FileSystem } from '../FileSystem'; +let _cachedHomeFolder: string | undefined; + /** * Returns the current user's home folder path. - * Throws if it cannot be determined. + * Throws if it cannot be determined. Successful results are cached. * @public */ export function getHomeFolder(): string { + if (_cachedHomeFolder !== undefined) { + return _cachedHomeFolder; + } + const unresolvedUserFolder: string | undefined = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME']; const dirError: string = "Unable to determine the current user's home directory"; @@ -23,5 +29,7 @@ export function getHomeFolder(): string { throw new Error(dirError); } + _cachedHomeFolder = homeFolder; + return homeFolder; } From 24b1ffdbc28f4ee85bc85905adbf3f6197072e00 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 22:39:39 +0000 Subject: [PATCH 12/14] Remove reexport of areDeepEqual --- libraries/rush-lib/src/utilities/objectUtilities.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libraries/rush-lib/src/utilities/objectUtilities.ts b/libraries/rush-lib/src/utilities/objectUtilities.ts index e282e6d6659..7261c88ef4d 100644 --- a/libraries/rush-lib/src/utilities/objectUtilities.ts +++ b/libraries/rush-lib/src/utilities/objectUtilities.ts @@ -1,13 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { Objects } from '@rushstack/node-core-library'; - -/** - * Determines if two objects are deeply equal. - */ -export const objectsAreDeepEqual: typeof Objects.areDeepEqual = Objects.areDeepEqual; - export function cloneDeep(obj: TObject): TObject { return cloneDeepInner(obj, new Set()); } From 21dc28cb4a26a94d2fe5b8c0c122fcf708e95819 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 22:39:57 +0000 Subject: [PATCH 13/14] Update API md --- common/reviews/api/credential-cache.api.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/reviews/api/credential-cache.api.md b/common/reviews/api/credential-cache.api.md index fa2c0647447..2030b68c5b3 100644 --- a/common/reviews/api/credential-cache.api.md +++ b/common/reviews/api/credential-cache.api.md @@ -46,4 +46,7 @@ export interface ICredentialCacheOptions { supportEditing: boolean; } +// @public +export const RUSH_USER_FOLDER_NAME: '.rush-user'; + ``` From 080f45fec2ba17327dfdbdf1f70ecda539a58104 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 22 Oct 2025 23:03:49 +0000 Subject: [PATCH 14/14] Centralize polyfill --- common/reviews/api/credential-cache.api.md | 4 +--- common/reviews/api/node-core-library.api.md | 10 ++++++++++ .../credential-cache/src/CredentialCache.ts | 18 +++++++++++++----- libraries/node-core-library/src/Disposables.ts | 6 ++++++ .../node-core-library/src/disposables/index.ts | 4 ++++ .../src/disposables/polyfillDisposeSymbols.ts | 15 +++++++++++++++ libraries/node-core-library/src/index.ts | 2 ++ 7 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 libraries/node-core-library/src/Disposables.ts create mode 100644 libraries/node-core-library/src/disposables/index.ts create mode 100644 libraries/node-core-library/src/disposables/polyfillDisposeSymbols.ts diff --git a/common/reviews/api/credential-cache.api.md b/common/reviews/api/credential-cache.api.md index 2030b68c5b3..f6563cc246a 100644 --- a/common/reviews/api/credential-cache.api.md +++ b/common/reviews/api/credential-cache.api.md @@ -6,10 +6,8 @@ // @public (undocumented) export class CredentialCache implements Disposable { - // Warning: (ae-forgotten-export) The symbol "SYMBOL_DISPOSE" needs to be exported by the entry point index.d.ts - // // (undocumented) - [SYMBOL_DISPOSE](): void; + [Symbol.dispose](): void; // (undocumented) deleteCacheEntry(cacheId: string): void; // (undocumented) diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index a8440caf548..cc01c92c873 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -61,6 +61,13 @@ export type Brand = T & { __brand: BrandTag; }; +declare namespace Disposables { + export { + polyfillDisposeSymbols + } +} +export { Disposables } + // @public export enum Encoding { // (undocumented) @@ -854,6 +861,9 @@ export class Path { static isUnderOrEqual(childPath: string, parentFolderPath: string): boolean; } +// @public +function polyfillDisposeSymbols(): void; + // @public export enum PosixModeBits { AllExecute = 73, diff --git a/libraries/credential-cache/src/CredentialCache.ts b/libraries/credential-cache/src/CredentialCache.ts index 68bf10da706..bac9378e7a9 100644 --- a/libraries/credential-cache/src/CredentialCache.ts +++ b/libraries/credential-cache/src/CredentialCache.ts @@ -3,10 +3,21 @@ import * as path from 'node:path'; -import { FileSystem, JsonFile, JsonSchema, LockFile, User, Objects } from '@rushstack/node-core-library'; +import { + Disposables, + FileSystem, + JsonFile, + JsonSchema, + LockFile, + User, + Objects +} from '@rushstack/node-core-library'; import schemaJson from './schemas/credentials.schema.json'; +// Polyfill for node 18 +Disposables.polyfillDisposeSymbols(); + /** * The name of the default folder in the user's home directory where Rush stores user-specific data. * @public @@ -29,9 +40,6 @@ interface ICacheEntryJson { credentialMetadata?: object; } -// Polyfill for node 18 -const SYMBOL_DISPOSE: typeof Symbol.dispose = Symbol.dispose ?? Symbol.for('Symbol.dispose'); - /** * @public */ @@ -200,7 +208,7 @@ export class CredentialCache implements Disposable { } } - public [SYMBOL_DISPOSE](): void { + public [Symbol.dispose](): void { this.dispose(); } diff --git a/libraries/node-core-library/src/Disposables.ts b/libraries/node-core-library/src/Disposables.ts new file mode 100644 index 00000000000..8edc3e0e794 --- /dev/null +++ b/libraries/node-core-library/src/Disposables.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as Disposables from './disposables/index'; + +export { Disposables }; diff --git a/libraries/node-core-library/src/disposables/index.ts b/libraries/node-core-library/src/disposables/index.ts new file mode 100644 index 00000000000..f3aa7768ad1 --- /dev/null +++ b/libraries/node-core-library/src/disposables/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export { polyfillDisposeSymbols } from './polyfillDisposeSymbols'; diff --git a/libraries/node-core-library/src/disposables/polyfillDisposeSymbols.ts b/libraries/node-core-library/src/disposables/polyfillDisposeSymbols.ts new file mode 100644 index 00000000000..621807aa12f --- /dev/null +++ b/libraries/node-core-library/src/disposables/polyfillDisposeSymbols.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * @public + * Polyfill for `Symbol.dispose` and `Symbol.asyncDispose` for Node.js versions prior to 20 + */ +export function polyfillDisposeSymbols(): void { + (Symbol as { dispose?: typeof Symbol.dispose }).dispose ??= Symbol.for( + 'Symbol.dispose' + ) as typeof Symbol.dispose; + (Symbol as { asyncDispose?: typeof Symbol.asyncDispose }).asyncDispose ??= Symbol.for( + 'Symbol.asyncDispose' + ) as typeof Symbol.asyncDispose; +} diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index 7c2e32dfc10..c48a9c950a3 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -24,6 +24,8 @@ export { export { FileConstants, FolderConstants } from './Constants'; +export { Disposables } from './Disposables'; + export { Enum } from './Enum'; export { EnvironmentMap, type IEnvironmentEntry } from './EnvironmentMap';