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 20fc8c92b00..76c83427e7f 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -506,6 +506,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 f1478f76c26..eb60e505234 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -2365,6 +2365,10 @@ packages: dependencies: is-obj: 2.0.0 + /dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + /electron-to-chromium@1.5.68: resolution: {integrity: sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ==} dev: true @@ -6443,6 +6447,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 a69e49eb206..9c39522114c 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": "b48c35b584583839e5d4a9e651d78a779d206b2e", + "pnpmShrinkwrapHash": "8465c51e473155e55c752d1ba8a929a9dde031fd", "preferredVersionsHash": "54149ea3f01558a859c96dee2052b797d4defe68", - "packageJsonInjectedDependenciesHash": "a8af9805efae30377fcf83e1a927721f67381aee" + "packageJsonInjectedDependenciesHash": "eacd9bb78498347b62458a678a12bdca7293da5d" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index d013af1f49b..eebdf7a26d8 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -3309,6 +3309,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 @@ -17368,10 +17371,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==} @@ -22006,7 +22008,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 e378b89f728..afc6bf370d2 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": "9429dfa99a546617d190d3eb990cc5fbc6e0ac7c", + "pnpmShrinkwrapHash": "5f6737a24eea6a2afef9ab8a02f07ec3306addd8", "preferredVersionsHash": "54149ea3f01558a859c96dee2052b797d4defe68" } diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 6958d0b585f..c4e93557f39 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -245,6 +245,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 daeb2096afa..bf19358ef38 100644 --- a/libraries/rush-lib/package.json +++ b/libraries/rush-lib/package.json @@ -45,6 +45,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", @@ -55,6 +56,7 @@ "js-yaml": "~3.13.1", "npm-check": "~6.0.1", "npm-package-arg": "~6.1.0", + "pnpm-sync-lib": "0.2.9", "read-package-tree": "~5.1.5", "rxjs": "~6.6.7", "semver": "~7.5.4", @@ -63,8 +65,7 @@ "tapable": "2.2.1", "tar": "~6.2.1", "true-case-path": "~2.2.1", - "uuid": "~8.3.2", - "pnpm-sync-lib": "0.2.9" + "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 9c5b37e5d83..e261f52b59f 100644 --- a/libraries/rush-lib/src/cli/RushCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushCommandLineParser.ts @@ -16,6 +16,7 @@ import { Colorize, type ITerminal } from '@rushstack/terminal'; +import dotenv from 'dotenv'; import { RushConfiguration } from '../api/RushConfiguration'; import { RushConstants } from '../logic/RushConstants'; @@ -27,6 +28,7 @@ import { } from '../api/CommandLineConfiguration'; import { AddAction } from './actions/AddAction'; +import { AlertAction } from './actions/AlertAction'; import { ChangeAction } from './actions/ChangeAction'; import { CheckAction } from './actions/CheckAction'; import { DeployAction } from './actions/DeployAction'; @@ -34,6 +36,7 @@ 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 { ListAction } from './actions/ListAction'; import { PublishAction } from './actions/PublishAction'; @@ -43,12 +46,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,11 +60,11 @@ 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 { RushUserConfiguration } from '../api/RushUserConfiguration'; +import { EnvironmentConfiguration } from '../api/EnvironmentConfiguration'; /** * Options for `RushCommandLineParser`. @@ -113,17 +116,39 @@ 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); + + if (EnvironmentConfiguration.hasBeenValidated) { + throw new Error( + `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. + + if (rushJsonFilePath) { + this.rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFilePath); } } catch (error) { this._reportErrorAndSetExitCode(error as Error); @@ -131,7 +156,7 @@ export class RushCommandLineParser extends CommandLineParser { NodeJsCompatibility.warnAboutCompatibilityIssues({ isRushLib: true, - alreadyReportedNodeTooNewError: this._rushOptions.alreadyReportedNodeTooNewError, + alreadyReportedNodeTooNewError, rushConfiguration: this.rushConfiguration }); @@ -139,13 +164,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/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 6316c8fe6ce..2cdd0b69f6e 100644 --- a/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts +++ b/libraries/rush-lib/src/cli/test/RushCommandLineParser.test.ts @@ -27,6 +27,7 @@ import { FileSystem, JsonFile, Path } from '@rushstack/node-core-library'; 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)); @@ -40,6 +41,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/logic/test/CredentialCache.test.ts b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts index b2d8ac49a43..f93ec0dec05 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 } from '../CredentialCache'; -const FAKE_RUSH_USER_FOLDER: string = '~/.rush-user'; +const FAKE_RUSH_USER_FOLDER: string = 'temp/.test-rush-user'; const FAKE_CREDENTIALS_CACHE_FILE: string = `${FAKE_RUSH_USER_FOLDER}/credentials.json`; describe(CredentialCache.name, () => { 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;