diff --git a/apps/rush/bin/rush-pnpm b/apps/rush/bin/rush-pnpm old mode 100644 new mode 100755 diff --git a/common/changes/@microsoft/rush/dotenv_2025-03-10-22-13.json b/common/changes/@microsoft/rush/dotenv_2025-03-10-22-13.json new file mode 100644 index 00000000000..a0ee5311fd7 --- /dev/null +++ b/common/changes/@microsoft/rush/dotenv_2025-03-10-22-13.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add support for setting environment variables via `/.env` and `~/.rush-user/.env` files.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 1bc39a45f06..9f344afc501 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -562,6 +562,10 @@ "name": "doc-plugin-rush-stack", "allowedCategories": [ "libraries" ] }, + { + "name": "dotenv", + "allowedCategories": [ "libraries" ] + }, { "name": "eslint", "allowedCategories": [ "libraries", "tests", "vscode-extensions" ] diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index 4c2b9671427..be3df89044e 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -1406,7 +1406,7 @@ packages: fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.2 ts-api-utils: 2.0.1(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: @@ -1425,7 +1425,7 @@ packages: fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.2 ts-api-utils: 2.0.1(typescript@5.8.2) typescript: 5.8.2 transitivePeerDependencies: @@ -2462,6 +2462,10 @@ packages: dependencies: is-obj: 2.0.0 + /dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + /dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -7037,6 +7041,7 @@ packages: builtin-modules: 3.1.0 cli-table: 0.3.11 dependency-path: 9.2.8 + dotenv: 16.4.7 fast-glob: 3.3.2 figures: 3.0.0 git-repo-info: 2.1.1 diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index 6c98abe1563..f78c092824a 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": "e8913fc783fabd3c2743397b9e8494b7d39dce34", + "pnpmShrinkwrapHash": "5025d77ad0239eada0c6cfaf815da697ef809c24", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "8f951e62240e50b2601a58e625da39e294a7b50f" + "packageJsonInjectedDependenciesHash": "dd0075f3b513d4c2edb0a3192c25650cc1277d66" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 575d03d83f5..6f0051c4816 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -3605,6 +3605,9 @@ importers: dependency-path: specifier: ~9.2.8 version: 9.2.8 + dotenv: + specifier: ~16.4.7 + version: 16.4.7 fast-glob: specifier: ~3.3.1 version: 3.3.2 @@ -18537,10 +18540,9 @@ packages: engines: {node: '>=10'} dev: true - /dotenv@16.4.5: - resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + /dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dev: true /dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} @@ -23741,7 +23743,7 @@ packages: optional: true dependencies: chalk: 4.1.2 - dotenv: 16.4.5 + dotenv: 16.4.7 kysely: 0.21.6 micromatch: 4.0.5 minimist: 1.2.8 diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 7d1c539babc..b98a571ba09 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "79f96eccf6ba5dbdba5a8478dc7fa309e2747884", + "pnpmShrinkwrapHash": "f115668be4d66c7667ba49d56d3426211048e376", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9" } diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index a12154b0277..42c2823ebff 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -246,6 +246,7 @@ export class EnvironmentConfiguration { // @internal static _getRushGlobalFolderOverride(processEnv: IEnvironment): string | undefined; static get gitBinaryPath(): string | undefined; + static get hasBeenValidated(): boolean; // (undocumented) static parseBooleanEnvironmentVariable(name: string, value: string | undefined): boolean | undefined; static get pnpmStorePathOverride(): string | undefined; diff --git a/libraries/rush-lib/package.json b/libraries/rush-lib/package.json index 865b833ed4d..11cdff0a6ef 100644 --- a/libraries/rush-lib/package.json +++ b/libraries/rush-lib/package.json @@ -46,6 +46,7 @@ "builtin-modules": "~3.1.0", "cli-table": "~0.3.1", "dependency-path": "~9.2.8", + "dotenv": "~16.4.7", "fast-glob": "~3.3.1", "figures": "3.0.0", "git-repo-info": "~2.1.0", @@ -56,6 +57,7 @@ "js-yaml": "~3.13.1", "npm-check": "~6.0.1", "npm-package-arg": "~6.1.0", + "pnpm-sync-lib": "0.3.2", "read-package-tree": "~5.1.5", "rxjs": "~6.6.7", "semver": "~7.5.4", @@ -64,8 +66,7 @@ "tapable": "2.2.1", "tar": "~6.2.1", "true-case-path": "~2.2.1", - "uuid": "~8.3.2", - "pnpm-sync-lib": "0.3.2" + "uuid": "~8.3.2" }, "devDependencies": { "@pnpm/lockfile.types": "~1.0.3", diff --git a/libraries/rush-lib/src/api/EnvironmentConfiguration.ts b/libraries/rush-lib/src/api/EnvironmentConfiguration.ts index da883ddff60..47738daefcc 100644 --- a/libraries/rush-lib/src/api/EnvironmentConfiguration.ts +++ b/libraries/rush-lib/src/api/EnvironmentConfiguration.ts @@ -260,6 +260,13 @@ export class EnvironmentConfiguration { private static _tarBinaryPath: string | undefined; + /** + * If true, the environment configuration has been validated and initialized. + */ + public static get hasBeenValidated(): boolean { + return EnvironmentConfiguration._hasBeenValidated; + } + /** * An override for the common/temp folder path. */ diff --git a/libraries/rush-lib/src/api/RushUserConfiguration.ts b/libraries/rush-lib/src/api/RushUserConfiguration.ts index a81c4080ed7..656bbd86480 100644 --- a/libraries/rush-lib/src/api/RushUserConfiguration.ts +++ b/libraries/rush-lib/src/api/RushUserConfiguration.ts @@ -52,10 +52,6 @@ export class RushUserConfiguration { public static getRushUserFolderPath(): string { const homeFolderPath: string = Utilities.getHomeFolder(); - const rushUserSettingsFilePath: string = path.join( - homeFolderPath, - RushConstants.rushUserConfigurationFolderName - ); - return rushUserSettingsFilePath; + return `${homeFolderPath}/${RushConstants.rushUserConfigurationFolderName}`; } } diff --git a/libraries/rush-lib/src/cli/RushCommandLineParser.ts b/libraries/rush-lib/src/cli/RushCommandLineParser.ts index 02c7bcb285a..d21b637f931 100644 --- a/libraries/rush-lib/src/cli/RushCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushCommandLineParser.ts @@ -27,6 +27,8 @@ import { } from '../api/CommandLineConfiguration'; import { AddAction } from './actions/AddAction'; +import { AlertAction } from './actions/AlertAction'; +import { BridgePackageAction } from './actions/BridgePackageAction'; import { ChangeAction } from './actions/ChangeAction'; import { CheckAction } from './actions/CheckAction'; import { DeployAction } from './actions/DeployAction'; @@ -34,7 +36,9 @@ import { InitAction } from './actions/InitAction'; import { InitAutoinstallerAction } from './actions/InitAutoinstallerAction'; import { InitDeployAction } from './actions/InitDeployAction'; import { InstallAction } from './actions/InstallAction'; +import { InstallAutoinstallerAction } from './actions/InstallAutoinstallerAction'; import { LinkAction } from './actions/LinkAction'; +import { LinkPackageAction } from './actions/LinkPackageAction'; import { ListAction } from './actions/ListAction'; import { PublishAction } from './actions/PublishAction'; import { PurgeAction } from './actions/PurgeAction'; @@ -43,12 +47,12 @@ import { ScanAction } from './actions/ScanAction'; import { UnlinkAction } from './actions/UnlinkAction'; import { UpdateAction } from './actions/UpdateAction'; import { UpdateAutoinstallerAction } from './actions/UpdateAutoinstallerAction'; -import { VersionAction } from './actions/VersionAction'; import { UpdateCloudCredentialsAction } from './actions/UpdateCloudCredentialsAction'; import { UpgradeInteractiveAction } from './actions/UpgradeInteractiveAction'; -import { AlertAction } from './actions/AlertAction'; +import { VersionAction } from './actions/VersionAction'; import { GlobalScriptAction } from './scriptActions/GlobalScriptAction'; +import { PhasedScriptAction } from './scriptActions/PhasedScriptAction'; import type { IBaseScriptActionOptions } from './scriptActions/BaseScriptAction'; import { Telemetry } from '../logic/Telemetry'; @@ -57,13 +61,10 @@ import { NodeJsCompatibility } from '../logic/NodeJsCompatibility'; import { SetupAction } from './actions/SetupAction'; import { type ICustomCommandLineConfigurationInfo, PluginManager } from '../pluginFramework/PluginManager'; import { RushSession } from '../pluginFramework/RushSession'; -import { PhasedScriptAction } from './scriptActions/PhasedScriptAction'; import type { IBuiltInPluginConfiguration } from '../pluginFramework/PluginLoader/BuiltInPluginLoader'; import { InitSubspaceAction } from './actions/InitSubspaceAction'; import { RushAlerts } from '../utilities/RushAlerts'; -import { InstallAutoinstallerAction } from './actions/InstallAutoinstallerAction'; -import { LinkPackageAction } from './actions/LinkPackageAction'; -import { BridgePackageAction } from './actions/BridgePackageAction'; +import { initializeDotEnv } from '../logic/dotenv'; import { measureAsyncFn } from '../utilities/performance'; @@ -118,17 +119,24 @@ export class RushCommandLineParser extends CommandLineParser { description: 'Hide rush startup information' }); - this._terminalProvider = new ConsoleTerminalProvider(); - this._terminal = new Terminal(this._terminalProvider); + const terminalProvider: ConsoleTerminalProvider = new ConsoleTerminalProvider(); + this._terminalProvider = terminalProvider; + const terminal: Terminal = new Terminal(this._terminalProvider); + this._terminal = terminal; this._rushOptions = this._normalizeOptions(options || {}); + const { cwd, alreadyReportedNodeTooNewError, builtInPluginConfigurations } = this._rushOptions; + let rushJsonFilePath: string | undefined; try { - const rushJsonFilename: string | undefined = RushConfiguration.tryFindRushJsonLocation({ - startingFolder: this._rushOptions.cwd, + rushJsonFilePath = RushConfiguration.tryFindRushJsonLocation({ + startingFolder: cwd, showVerbose: !this._restrictConsoleOutput }); - if (rushJsonFilename) { - this.rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFilename); + + initializeDotEnv(terminal, rushJsonFilePath); + + if (rushJsonFilePath) { + this.rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFilePath); } } catch (error) { this._reportErrorAndSetExitCode(error as Error); @@ -136,7 +144,7 @@ export class RushCommandLineParser extends CommandLineParser { NodeJsCompatibility.warnAboutCompatibilityIssues({ isRushLib: true, - alreadyReportedNodeTooNewError: this._rushOptions.alreadyReportedNodeTooNewError, + alreadyReportedNodeTooNewError, rushConfiguration: this.rushConfiguration }); @@ -144,13 +152,13 @@ export class RushCommandLineParser extends CommandLineParser { this.rushSession = new RushSession({ getIsDebugMode: () => this.isDebug, - terminalProvider: this._terminalProvider + terminalProvider }); this.pluginManager = new PluginManager({ rushSession: this.rushSession, rushConfiguration: this.rushConfiguration, - terminal: this._terminal, - builtInPluginConfigurations: this._rushOptions.builtInPluginConfigurations, + terminal, + builtInPluginConfigurations, restrictConsoleOutput: this._restrictConsoleOutput, rushGlobalFolder: this.rushGlobalFolder }); diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index f054e4bbe00..ffee5ae280d 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -33,6 +33,7 @@ import { Utilities } from '../utilities/Utilities'; import type { Subspace } from '../api/Subspace'; import type { PnpmOptionsConfiguration } from '../logic/pnpm/PnpmOptionsConfiguration'; import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration'; +import { initializeDotEnv } from '../logic/dotenv'; const RUSH_SKIP_CHECKS_PARAMETER: string = '--rush-skip-checks'; @@ -78,10 +79,17 @@ export class RushPnpmCommandLineParser { this._terminal = terminal; // Are we in a Rush repo? - const rushConfiguration: RushConfiguration | undefined = RushConfiguration.tryLoadFromDefaultLocation({ + const rushJsonFilePath: string | undefined = RushConfiguration.tryFindRushJsonLocation({ // showVerbose is false because the logging message may break JSON output showVerbose: false }); + + initializeDotEnv(terminal, rushJsonFilePath); + + const rushConfiguration: RushConfiguration | undefined = rushJsonFilePath + ? RushConfiguration.loadFromConfigurationFile(rushJsonFilePath) + : undefined; + NodeJsCompatibility.warnAboutCompatibilityIssues({ isRushLib: true, alreadyReportedNodeTooNewError: !!options.alreadyReportedNodeTooNewError, diff --git a/libraries/rush-lib/src/cli/RushXCommandLine.ts b/libraries/rush-lib/src/cli/RushXCommandLine.ts index 9ab971c89c9..4542b8dad60 100644 --- a/libraries/rush-lib/src/cli/RushXCommandLine.ts +++ b/libraries/rush-lib/src/cli/RushXCommandLine.ts @@ -25,6 +25,7 @@ import { Event } from '../api/EventHooks'; import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration'; import { RushConstants } from '../logic/RushConstants'; import { PnpmSyncUtilities } from '../utilities/PnpmSyncUtilities'; +import { initializeDotEnv } from '../logic/dotenv'; interface IRushXCommandLineArguments { /** @@ -77,18 +78,31 @@ export class RushXCommandLine { public static async launchRushXAsync(launcherVersion: string, options: ILaunchOptions): Promise { try { const rushxArguments: IRushXCommandLineArguments = RushXCommandLine._parseCommandLineArguments(); - const rushConfiguration: RushConfiguration | undefined = RushConfiguration.tryLoadFromDefaultLocation({ + const rushJsonFilePath: string | undefined = RushConfiguration.tryFindRushJsonLocation({ showVerbose: false }); + const { isDebug, help, ignoreHooks } = rushxArguments; + + const terminalProvider: ITerminalProvider = new ConsoleTerminalProvider({ + debugEnabled: isDebug, + verboseEnabled: isDebug + }); + const terminal: ITerminal = new Terminal(terminalProvider); + + initializeDotEnv(terminal, rushJsonFilePath); + + const rushConfiguration: RushConfiguration | undefined = rushJsonFilePath + ? RushConfiguration.loadFromConfigurationFile(rushJsonFilePath) + : undefined; const eventHooksManager: EventHooksManager | undefined = rushConfiguration ? new EventHooksManager(rushConfiguration) : undefined; const suppressHooks: boolean = process.env[EnvironmentVariableNames._RUSH_RECURSIVE_RUSHX_CALL] === '1'; - const attemptHooks: boolean = !suppressHooks && !rushxArguments.help; + const attemptHooks: boolean = !suppressHooks && !help; if (attemptHooks) { try { - eventHooksManager?.handle(Event.preRushx, rushxArguments.isDebug, rushxArguments.ignoreHooks); + eventHooksManager?.handle(Event.preRushx, isDebug, ignoreHooks); } catch (error) { // eslint-disable-next-line no-console console.error(Colorize.red('PreRushx hook error: ' + (error as Error).message)); @@ -98,10 +112,10 @@ export class RushXCommandLine { // promise exception), so we start with the assumption that the exit code is 1 // and set it to 0 only on success. process.exitCode = 1; - await RushXCommandLine._launchRushXInternalAsync(rushxArguments, rushConfiguration, options); + await RushXCommandLine._launchRushXInternalAsync(terminal, rushxArguments, rushConfiguration, options); if (attemptHooks) { try { - eventHooksManager?.handle(Event.postRushx, rushxArguments.isDebug, rushxArguments.ignoreHooks); + eventHooksManager?.handle(Event.postRushx, isDebug, ignoreHooks); } catch (error) { // eslint-disable-next-line no-console console.error(Colorize.red('PostRushx hook error: ' + (error as Error).message)); @@ -122,6 +136,7 @@ export class RushXCommandLine { } private static async _launchRushXInternalAsync( + terminal: ITerminal, rushxArguments: IRushXCommandLineArguments, rushConfiguration: RushConfiguration | undefined, options: ILaunchOptions @@ -218,12 +233,6 @@ export class RushXCommandLine { } }); - const terminalProvider: ITerminalProvider = new ConsoleTerminalProvider({ - debugEnabled: rushxArguments.isDebug, - verboseEnabled: rushxArguments.isDebug - }); - const terminal: ITerminal = new Terminal(terminalProvider); - if (rushConfiguration?.isPnpm && rushConfiguration?.experimentsConfiguration) { const { configuration: experiments } = rushConfiguration?.experimentsConfiguration; diff --git a/libraries/rush-lib/src/cli/actions/test/AddAction.test.ts b/libraries/rush-lib/src/cli/actions/test/AddAction.test.ts index f6878621cc8..e41934ed1f0 100644 --- a/libraries/rush-lib/src/cli/actions/test/AddAction.test.ts +++ b/libraries/rush-lib/src/cli/actions/test/AddAction.test.ts @@ -3,11 +3,13 @@ import '../../test/mockRushCommandLineParser'; +import { LockFile } from '@rushstack/node-core-library'; + import { PackageJsonUpdater } from '../../../logic/PackageJsonUpdater'; import type { IPackageJsonUpdaterRushAddOptions } from '../../../logic/PackageJsonUpdaterTypes'; import { RushCommandLineParser } from '../../RushCommandLineParser'; import { AddAction } from '../AddAction'; -import { LockFile } from '@rushstack/node-core-library'; +import { EnvironmentConfiguration } from '../../../api/EnvironmentConfiguration'; describe(AddAction.name, () => { describe('basic "rush add" tests', () => { @@ -32,6 +34,7 @@ describe(AddAction.name, () => { jest.clearAllMocks(); process.exitCode = oldExitCode; process.argv = oldArgs; + EnvironmentConfiguration.reset(); }); describe("'add' action", () => { diff --git a/libraries/rush-lib/src/cli/actions/test/RemoveAction.test.ts b/libraries/rush-lib/src/cli/actions/test/RemoveAction.test.ts index 8fe5535db79..f6a32ac87b4 100644 --- a/libraries/rush-lib/src/cli/actions/test/RemoveAction.test.ts +++ b/libraries/rush-lib/src/cli/actions/test/RemoveAction.test.ts @@ -3,13 +3,15 @@ import '../../test/mockRushCommandLineParser'; +import { LockFile } from '@rushstack/node-core-library'; + import { PackageJsonUpdater } from '../../../logic/PackageJsonUpdater'; import type { IPackageJsonUpdaterRushRemoveOptions } from '../../../logic/PackageJsonUpdaterTypes'; import { RushCommandLineParser } from '../../RushCommandLineParser'; import { RemoveAction } from '../RemoveAction'; import { VersionMismatchFinderProject } from '../../../logic/versionMismatch/VersionMismatchFinderProject'; import { DependencyType } from '../../../api/PackageJsonEditor'; -import { LockFile } from '@rushstack/node-core-library'; +import { EnvironmentConfiguration } from '../../../api/EnvironmentConfiguration'; describe(RemoveAction.name, () => { describe('basic "rush remove" tests', () => { @@ -36,6 +38,7 @@ describe(RemoveAction.name, () => { jest.clearAllMocks(); process.exitCode = oldExitCode; process.argv = oldArgs; + EnvironmentConfiguration.reset(); }); describe("'remove' action", () => { diff --git a/libraries/rush-lib/src/cli/test/CommandLineHelp.test.ts b/libraries/rush-lib/src/cli/test/CommandLineHelp.test.ts index 401fa92d07e..78ce4ee8557 100644 --- a/libraries/rush-lib/src/cli/test/CommandLineHelp.test.ts +++ b/libraries/rush-lib/src/cli/test/CommandLineHelp.test.ts @@ -4,6 +4,7 @@ import { AnsiEscape } from '@rushstack/terminal'; import { RushCommandLineParser } from '../RushCommandLineParser'; +import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; describe('CommandLineHelp', () => { let oldCwd: string | undefined; @@ -33,6 +34,8 @@ describe('CommandLineHelp', () => { if (oldCwd) { process.chdir(oldCwd); } + + EnvironmentConfiguration.reset(); }); it('prints the global help', () => { diff --git a/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts b/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts index 6219af51d54..a8122091dd1 100644 --- a/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts +++ b/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts @@ -32,6 +32,7 @@ import type { IDetailedRepoState } from '@rushstack/package-deps-hash'; import { Autoinstaller } from '../../logic/Autoinstaller'; import type { ITelemetryData } from '../../logic/Telemetry'; import { getCommandLineParserInstanceAsync } from './TestUtils'; +import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; function pathEquals(actual: string, expected: string): void { expect(Path.convertToSlashes(actual)).toEqual(Path.convertToSlashes(expected)); @@ -45,6 +46,7 @@ describe('RushCommandLineParser', () => { describe('execute', () => { afterEach(() => { jest.clearAllMocks(); + EnvironmentConfiguration.reset(); }); describe('in basic repo', () => { diff --git a/libraries/rush-lib/src/cli/test/RushPluginCommandLineParameters.test.ts b/libraries/rush-lib/src/cli/test/RushPluginCommandLineParameters.test.ts index a48e06de2be..c8141d11cd9 100644 --- a/libraries/rush-lib/src/cli/test/RushPluginCommandLineParameters.test.ts +++ b/libraries/rush-lib/src/cli/test/RushPluginCommandLineParameters.test.ts @@ -7,6 +7,7 @@ import path from 'path'; import { FileSystem, LockFile } from '@rushstack/node-core-library'; import { RushCommandLineParser } from '../RushCommandLineParser'; import { Autoinstaller } from '../../logic/Autoinstaller'; +import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; describe('PluginCommandLineParameters', () => { let originCWD: string | undefined; @@ -61,6 +62,8 @@ describe('PluginCommandLineParameters', () => { originCWD = undefined; process.argv = _argv; } + + EnvironmentConfiguration.reset(); }); afterAll(() => { diff --git a/libraries/rush-lib/src/cli/test/RushXCommandLine.test.ts b/libraries/rush-lib/src/cli/test/RushXCommandLine.test.ts index 35e2c2b5c1e..0bb398c4698 100644 --- a/libraries/rush-lib/src/cli/test/RushXCommandLine.test.ts +++ b/libraries/rush-lib/src/cli/test/RushXCommandLine.test.ts @@ -1,6 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +jest.mock('../../logic/dotenv', () => ({ + initializeDotEnv: () => {} +})); + import { PackageJsonLookup } from '@rushstack/node-core-library'; import { Utilities } from '../../utilities/Utilities'; @@ -61,7 +65,8 @@ describe(RushXCommandLine.name, () => { return projects.find((project) => project.projectFolder === path); } } as RushConfiguration; - jest.spyOn(RushConfiguration, 'tryLoadFromDefaultLocation').mockReturnValue(rushConfiguration); + jest.spyOn(RushConfiguration, 'tryFindRushJsonLocation').mockReturnValue('/Users/jdoe/bigrepo'); + jest.spyOn(RushConfiguration, 'loadFromConfigurationFile').mockReturnValue(rushConfiguration); // Mock command execution executeLifecycleCommandMock = jest.spyOn(Utilities, 'executeLifecycleCommand'); diff --git a/libraries/rush-lib/src/logic/dotenv.ts b/libraries/rush-lib/src/logic/dotenv.ts new file mode 100644 index 00000000000..cee2f677f28 --- /dev/null +++ b/libraries/rush-lib/src/logic/dotenv.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import path from 'node:path'; +import dotenv from 'dotenv'; +import type { ITerminal } from '@rushstack/terminal'; + +import { RushUserConfiguration } from '../api/RushUserConfiguration'; +import { EnvironmentConfiguration } from '../api/EnvironmentConfiguration'; + +export function initializeDotEnv(terminal: ITerminal, rushJsonFilePath: string | undefined): void { + if (EnvironmentConfiguration.hasBeenValidated) { + throw terminal.writeWarningLine( + `The ${EnvironmentConfiguration.name} was initialized before .env files were loaded. Rush environment ` + + 'variables may have unexpected values.' + ); + } + + if (rushJsonFilePath) { + const rushJsonFolder: string = path.dirname(rushJsonFilePath); + dotenv.config({ path: `${rushJsonFolder}/.env` }); + } + + const rushUserFolder: string = RushUserConfiguration.getRushUserFolderPath(); + dotenv.config({ path: `${rushUserFolder}/.env` }); + + // TODO: Consider adding support for repo-specific `.rush-user` `.env` files. +} diff --git a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts index fe7e92fae97..46ce1bf6ccd 100644 --- a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts +++ b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts @@ -5,7 +5,7 @@ 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 = '~/.rush-user'; +const FAKE_RUSH_USER_FOLDER: string = 'temp/.rush-user'; interface IPathsTestCase extends Required> { testCaseName: string; diff --git a/libraries/rush-lib/src/utilities/Utilities.ts b/libraries/rush-lib/src/utilities/Utilities.ts index d521640b543..14436e687fc 100644 --- a/libraries/rush-lib/src/utilities/Utilities.ts +++ b/libraries/rush-lib/src/utilities/Utilities.ts @@ -155,20 +155,28 @@ interface ICreateEnvironmentForRushCommandOptions { export class Utilities { public static syncNpmrc: typeof syncNpmrc = syncNpmrc; + private static _homeFolder: string | undefined; + /** * Get the user's home directory. On windows this looks something like "C:\users\username\" and on UNIX * this looks something like "/home/username/" */ public static 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); + let homeFolder: string | undefined = Utilities._homeFolder; + if (!homeFolder) { + 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); + } + + homeFolder = path.resolve(unresolvedUserFolder); + if (!FileSystem.exists(homeFolder)) { + throw new Error(dirError); + } + + Utilities._homeFolder = homeFolder; } return homeFolder;