diff --git a/common/changes/@microsoft/rush/feature-support_fallback_syntax_in_npmrc_2024-12-04-13-22.json b/common/changes/@microsoft/rush/feature-support_fallback_syntax_in_npmrc_2024-12-04-13-22.json new file mode 100644 index 00000000000..c1ed42db1e7 --- /dev/null +++ b/common/changes/@microsoft/rush/feature-support_fallback_syntax_in_npmrc_2024-12-04-13-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Support fallback syntax in `.npmrc` files if the package manager is PNPM. See https://pnpm.io/npmrc", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/feature-support_fallback_syntax_in_npmrc_2024-12-10-20-47.json b/common/changes/@microsoft/rush/feature-support_fallback_syntax_in_npmrc_2024-12-10-20-47.json new file mode 100644 index 00000000000..ed724209996 --- /dev/null +++ b/common/changes/@microsoft/rush/feature-support_fallback_syntax_in_npmrc_2024-12-10-20-47.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add an `.isPnpm` property to `RushConfiguration` that is set to true if the package manager for the Rush repo is PNPM.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 2ca78be7635..2fe19c11b06 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1222,6 +1222,7 @@ export class RushConfiguration { readonly gitTagSeparator: string | undefined; readonly gitVersionBumpCommitMessage: string | undefined; readonly hotfixChangeEnabled: boolean; + readonly isPnpm: boolean; static loadFromConfigurationFile(rushJsonFilename: string): RushConfiguration; // (undocumented) static loadFromDefaultLocation(options?: ITryFindRushJsonLocationOptions): RushConfiguration; diff --git a/libraries/rush-lib/src/api/RushConfiguration.ts b/libraries/rush-lib/src/api/RushConfiguration.ts index deee29cb4ae..feac52b0c18 100644 --- a/libraries/rush-lib/src/api/RushConfiguration.ts +++ b/libraries/rush-lib/src/api/RushConfiguration.ts @@ -251,6 +251,11 @@ export class RushConfiguration { */ public readonly packageManager!: PackageManagerName; + /** + * If true, the repository is using PNPM as its package manager. + */ + public readonly isPnpm!: boolean; + /** * {@inheritdoc PackageManager} * @@ -698,16 +703,20 @@ export class RushConfiguration { // TODO: Add an actual "packageManager" field in rush.json const packageManagerFields: string[] = []; + this.isPnpm = false; if (rushConfigurationJson.npmVersion) { this.packageManager = 'npm'; this.packageManagerOptions = this.npmOptions; packageManagerFields.push('npmVersion'); } + if (rushConfigurationJson.pnpmVersion) { this.packageManager = 'pnpm'; + this.isPnpm = true; this.packageManagerOptions = this.pnpmOptions; packageManagerFields.push('pnpmVersion'); } + if (rushConfigurationJson.yarnVersion) { this.packageManager = 'yarn'; this.packageManagerOptions = this.yarnOptions; diff --git a/libraries/rush-lib/src/cli/RushXCommandLine.ts b/libraries/rush-lib/src/cli/RushXCommandLine.ts index ee7ddad1115..a5a49f964fc 100644 --- a/libraries/rush-lib/src/cli/RushXCommandLine.ts +++ b/libraries/rush-lib/src/cli/RushXCommandLine.ts @@ -224,7 +224,7 @@ export class RushXCommandLine { }); const terminal: ITerminal = new Terminal(terminalProvider); - if (rushConfiguration?.packageManager === 'pnpm' && rushConfiguration?.experimentsConfiguration) { + if (rushConfiguration?.isPnpm && rushConfiguration?.experimentsConfiguration) { const { configuration: experiments } = rushConfiguration?.experimentsConfiguration; if (experiments?.usePnpmSyncForInjectedDependencies) { diff --git a/libraries/rush-lib/src/cli/actions/DeployAction.ts b/libraries/rush-lib/src/cli/actions/DeployAction.ts index c254184840e..5f67445dc8a 100644 --- a/libraries/rush-lib/src/cli/actions/DeployAction.ts +++ b/libraries/rush-lib/src/cli/actions/DeployAction.ts @@ -159,7 +159,7 @@ export class DeployAction extends BaseRushAction { } const projects: RushConfigurationProject[] = this.rushConfiguration.projects; - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { const currentlyInstalledVariant: string | undefined = await this.rushConfiguration.getCurrentlyInstalledVariantAsync(); for (const project of projects) { diff --git a/libraries/rush-lib/src/cli/actions/InstallAction.ts b/libraries/rush-lib/src/cli/actions/InstallAction.ts index 945c12337ca..51835887cd0 100644 --- a/libraries/rush-lib/src/cli/actions/InstallAction.ts +++ b/libraries/rush-lib/src/cli/actions/InstallAction.ts @@ -49,7 +49,7 @@ export class InstallAction extends BaseInstallAction { description: `Only check the validity of the shrinkwrap file without performing an install.` }); - if (this.rushConfiguration?.packageManager === 'pnpm') { + if (this.rushConfiguration?.isPnpm) { this._resolutionOnlyParameter = this.defineFlagParameter({ parameterLongName: '--resolution-only', description: `Only perform dependency resolution, useful for ensuring peer dependendencies are up to date. Note that this flag is only supported when using the pnpm package manager.` diff --git a/libraries/rush-lib/src/cli/actions/PublishAction.ts b/libraries/rush-lib/src/cli/actions/PublishAction.ts index 12170823a0f..1f3cf062a7b 100644 --- a/libraries/rush-lib/src/cli/actions/PublishAction.ts +++ b/libraries/rush-lib/src/cli/actions/PublishAction.ts @@ -239,7 +239,7 @@ export class PublishAction extends BaseRushAction { this._validate(); - this._addNpmPublishHome(); + this._addNpmPublishHome(this.rushConfiguration.isPnpm); const git: Git = new Git(this.rushConfiguration); const publishGit: PublishGit = new PublishGit(git, this._targetBranch.value); @@ -455,7 +455,7 @@ export class PublishAction extends BaseRushAction { args.push(`--access`, this._npmAccessLevel.value); } - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { // PNPM 4.11.0 introduced a feature that may interrupt publishing and prompt the user for input. // See this issue for details: https://github.com/microsoft/rushstack/issues/1940 args.push('--no-git-checks'); @@ -582,7 +582,7 @@ export class PublishAction extends BaseRushAction { } } - private _addNpmPublishHome(): void { + private _addNpmPublishHome(supportEnvVarFallbackSyntax: boolean): void { // Create "common\temp\publish-home" folder, if it doesn't exist Utilities.createFolderWithRetry(this._targetNpmrcPublishFolder); @@ -590,7 +590,8 @@ export class PublishAction extends BaseRushAction { Utilities.syncNpmrc({ sourceNpmrcFolder: this.rushConfiguration.commonRushConfigFolder, targetNpmrcFolder: this._targetNpmrcPublishFolder, - useNpmrcPublish: true + useNpmrcPublish: true, + supportEnvVarFallbackSyntax }); } diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 6eed33d9d00..5824689d613 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -466,10 +466,7 @@ export class PhasedScriptAction extends BaseScriptAction { } const { configuration: experiments } = this.rushConfiguration.experimentsConfiguration; - if ( - this.rushConfiguration?.packageManager === 'pnpm' && - experiments?.usePnpmSyncForInjectedDependencies - ) { + if (this.rushConfiguration?.isPnpm && experiments?.usePnpmSyncForInjectedDependencies) { const { PnpmSyncCopyOperationPlugin } = await import( '../../logic/operations/PnpmSyncCopyOperationPlugin' ); diff --git a/libraries/rush-lib/src/logic/Autoinstaller.ts b/libraries/rush-lib/src/logic/Autoinstaller.ts index 3b04e6c0cca..8dd5b0e4dbf 100644 --- a/libraries/rush-lib/src/logic/Autoinstaller.ts +++ b/libraries/rush-lib/src/logic/Autoinstaller.ts @@ -137,7 +137,8 @@ export class Autoinstaller { // Copy: .../common/autoinstallers/my-task/.npmrc Utilities.syncNpmrc({ sourceNpmrcFolder: this._rushConfiguration.commonRushConfigFolder, - targetNpmrcFolder: autoinstallerFullPath + targetNpmrcFolder: autoinstallerFullPath, + supportEnvVarFallbackSyntax: this._rushConfiguration.isPnpm }); this._logIfConsoleOutputIsNotRestricted( @@ -193,7 +194,7 @@ export class Autoinstaller { oldFileContents = FileSystem.readFile(this.shrinkwrapFilePath, { convertLineEndings: NewlineKind.Lf }); this._logIfConsoleOutputIsNotRestricted('Deleting ' + this.shrinkwrapFilePath); await FileSystem.deleteFileAsync(this.shrinkwrapFilePath); - if (this._rushConfiguration.packageManager === 'pnpm') { + if (this._rushConfiguration.isPnpm) { // Workaround for https://github.com/pnpm/pnpm/issues/1890 // // When "rush update-autoinstaller" is run, Rush deletes "common/autoinstallers/my-task/pnpm-lock.yaml" @@ -222,7 +223,8 @@ export class Autoinstaller { Utilities.syncNpmrc({ sourceNpmrcFolder: this._rushConfiguration.commonRushConfigFolder, - targetNpmrcFolder: this.folderFullPath + targetNpmrcFolder: this.folderFullPath, + supportEnvVarFallbackSyntax: this._rushConfiguration.isPnpm }); await Utilities.executeCommandAsync({ diff --git a/libraries/rush-lib/src/logic/InstallManagerFactory.ts b/libraries/rush-lib/src/logic/InstallManagerFactory.ts index 1bb46cb9d9d..f68de2d8c0c 100644 --- a/libraries/rush-lib/src/logic/InstallManagerFactory.ts +++ b/libraries/rush-lib/src/logic/InstallManagerFactory.ts @@ -17,7 +17,7 @@ export class InstallManagerFactory { options: IInstallManagerOptions ): Promise { if ( - rushConfiguration.packageManager === 'pnpm' && + rushConfiguration.isPnpm && rushConfiguration.pnpmOptions && rushConfiguration.pnpmOptions.useWorkspaces ) { diff --git a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts index 6c820c9ca78..dbcd522c123 100644 --- a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts +++ b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts @@ -149,9 +149,7 @@ export class ProjectChangeAnalyzer { return new Set(rushConfiguration.projects); } - const { packageManager } = rushConfiguration; - - if (packageManager === 'pnpm') { + if (rushConfiguration.isPnpm) { const currentShrinkwrap: PnpmShrinkwrapFile | undefined = PnpmShrinkwrapFile.loadFromFile(fullShrinkwrapPath); @@ -256,7 +254,7 @@ export class ProjectChangeAnalyzer { // Include project shrinkwrap files as part of the computation const additionalRelativePathsToHash: string[] = []; const globalAdditionalFiles: string[] = []; - if (rushConfiguration.packageManager === 'pnpm') { + if (rushConfiguration.isPnpm) { await Async.forEachAsync(rushConfiguration.projects, async (project: RushConfigurationProject) => { const projectShrinkwrapFilePath: string = BaseProjectShrinkwrapFile.getFilePathForProject(project); if (!(await FileSystem.existsAsync(projectShrinkwrapFilePath))) { diff --git a/libraries/rush-lib/src/logic/PurgeManager.ts b/libraries/rush-lib/src/logic/PurgeManager.ts index 423d1ca388d..80081d21d1d 100644 --- a/libraries/rush-lib/src/logic/PurgeManager.ts +++ b/libraries/rush-lib/src/logic/PurgeManager.ts @@ -88,7 +88,7 @@ export class PurgeManager { ); if ( - this._rushConfiguration.packageManager === 'pnpm' && + this._rushConfiguration.isPnpm && this._rushConfiguration.pnpmOptions.pnpmStore === 'global' && this._rushConfiguration.pnpmOptions.pnpmStorePath ) { diff --git a/libraries/rush-lib/src/logic/RepoStateFile.ts b/libraries/rush-lib/src/logic/RepoStateFile.ts index 3bdff9898ed..3d875afd66c 100644 --- a/libraries/rush-lib/src/logic/RepoStateFile.ts +++ b/libraries/rush-lib/src/logic/RepoStateFile.ts @@ -162,7 +162,7 @@ export class RepoStateFile { // Only support saving the pnpm shrinkwrap hash if it was enabled const preventShrinkwrapChanges: boolean = - rushConfiguration.packageManager === 'pnpm' && + rushConfiguration.isPnpm && rushConfiguration.pnpmOptions && rushConfiguration.pnpmOptions.preventManualShrinkwrapChanges; if (preventShrinkwrapChanges) { @@ -200,7 +200,7 @@ export class RepoStateFile { this._modified = true; } - if (rushConfiguration.packageManager === 'pnpm' && rushConfiguration.subspacesFeatureEnabled) { + if (rushConfiguration.isPnpm && rushConfiguration.subspacesFeatureEnabled) { const packageJsonInjectedDependenciesHash: string | undefined = subspace.getPackageJsonInjectedDependenciesHash(variant); diff --git a/libraries/rush-lib/src/logic/SetupChecks.ts b/libraries/rush-lib/src/logic/SetupChecks.ts index f85a2386664..9f60b71708e 100644 --- a/libraries/rush-lib/src/logic/SetupChecks.ts +++ b/libraries/rush-lib/src/logic/SetupChecks.ts @@ -40,7 +40,7 @@ export class SetupChecks { private static _validate(rushConfiguration: RushConfiguration): string | undefined { // Check for outdated tools - if (rushConfiguration.packageManager === 'pnpm') { + if (rushConfiguration.isPnpm) { if (semver.lt(rushConfiguration.packageManagerToolVersion, MINIMUM_SUPPORTED_PNPM_VERSION)) { return ( `The ${RushConstants.rushJsonFilename} file requests PNPM version ` + diff --git a/libraries/rush-lib/src/logic/StandardScriptUpdater.ts b/libraries/rush-lib/src/logic/StandardScriptUpdater.ts index 79bc66b6128..5a4fda349c7 100644 --- a/libraries/rush-lib/src/logic/StandardScriptUpdater.ts +++ b/libraries/rush-lib/src/logic/StandardScriptUpdater.ts @@ -87,7 +87,7 @@ const _pnpmOnlyScripts: IScriptSpecifier[] = [ ]; const getScripts = (rushConfiguration: RushConfiguration): IScriptSpecifier[] => { - if (rushConfiguration.packageManager === 'pnpm') { + if (rushConfiguration.isPnpm) { return _scripts.concat(_pnpmOnlyScripts); } diff --git a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts index c0d081cc23b..dcf634229e8 100644 --- a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts +++ b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts @@ -283,7 +283,7 @@ export abstract class BaseInstallManager { const { configuration: experiments } = this.rushConfiguration.experimentsConfiguration; // if usePnpmSyncForInjectedDependencies is true // the pnpm-sync will generate the pnpm-sync.json based on lockfile - if (this.rushConfiguration.packageManager === 'pnpm' && experiments?.usePnpmSyncForInjectedDependencies) { + if (this.rushConfiguration.isPnpm && experiments?.usePnpmSyncForInjectedDependencies) { const pnpmLockfilePath: string = subspace.getTempShrinkwrapFilename(); const dotPnpmFolder: string = `${subspace.getSubspaceTempFolderPath()}/node_modules/.pnpm`; @@ -401,7 +401,7 @@ export abstract class BaseInstallManager { // Add pnpm-config.json file to the potentially changed files list. potentiallyChangedFiles.push(subspace.getPnpmConfigFilePath()); - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { // If the repo is using pnpmfile.js, consider that also const pnpmFileFilePath: string = subspace.getPnpmfilePath(variant); const pnpmFileExists: boolean = await FileSystem.existsAsync(pnpmFileFilePath); @@ -536,7 +536,8 @@ export abstract class BaseInstallManager { sourceNpmrcFolder: subspace.getSubspaceConfigFolderPath(), targetNpmrcFolder: subspace.getSubspaceTempFolderPath(), linesToPrepend: extraNpmrcLines, - createIfMissing: this.rushConfiguration.subspacesFeatureEnabled + createIfMissing: this.rushConfiguration.subspacesFeatureEnabled, + supportEnvVarFallbackSyntax: this.rushConfiguration.isPnpm }); this._syncNpmrcAlreadyCalled = true; @@ -544,7 +545,7 @@ export abstract class BaseInstallManager { ? crypto.createHash('sha1').update(npmrcText).digest('hex') : undefined; - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { // Copy the committed patches folder if using pnpm const commonTempPnpmPatchesFolder: string = `${subspace.getSubspaceTempFolderPath()}/${ RushConstants.pnpmPatchesFolderName @@ -599,7 +600,7 @@ export abstract class BaseInstallManager { // Shim support for pnpmfile in. // Additionally when in workspaces, the shim implements support for common versions. - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { await PnpmfileConfiguration.writeCommonTempPnpmfileShimAsync( this.rushConfiguration, subspace.getSubspaceTempFolderPath(), @@ -837,7 +838,7 @@ ${gitLfsHookHandling} if (collectLogFile) { args.push('--verbose'); } - } else if (this.rushConfiguration.packageManager === 'pnpm') { + } else if (this.rushConfiguration.isPnpm) { // Only explicitly define the store path if `pnpmStore` is using the default, or has been set to // 'local'. If `pnpmStore` = 'global', then allow PNPM to use the system's default // path. In all cases, this will be overridden by RUSH_PNPM_STORE_PATH @@ -912,7 +913,8 @@ ${gitLfsHookHandling} */ const isAutoInstallPeersInNpmrc: boolean = isVariableSetInNpmrcFile( subspace.getSubspaceConfigFolderPath(), - 'auto-install-peers' + 'auto-install-peers', + this.rushConfiguration.isPnpm ); let autoInstallPeers: boolean | undefined = this.rushConfiguration.pnpmOptions.autoInstallPeers; @@ -940,7 +942,8 @@ ${gitLfsHookHandling} */ const isResolutionModeInNpmrc: boolean = isVariableSetInNpmrcFile( subspace.getSubspaceConfigFolderPath(), - 'resolution-mode' + 'resolution-mode', + this.rushConfiguration.isPnpm ); let resolutionMode: PnpmResolutionMode | undefined = this.rushConfiguration.pnpmOptions.resolutionMode; @@ -1116,7 +1119,7 @@ ${gitLfsHookHandling} // Otherwise delete the temporary file FileSystem.deleteFile(subspace.getTempShrinkwrapFilename()); - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { // Workaround for https://github.com/pnpm/pnpm/issues/1890 // // When "rush update --full" is run, Rush deletes "common/temp/pnpm-lock.yaml" diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index f18605a61f3..40f3354879c 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -50,7 +50,7 @@ export class InstallHelpers { version: '0.0.0' }; - if (rushConfiguration.packageManager === 'pnpm') { + if (rushConfiguration.isPnpm) { const pnpmOptions: PnpmOptionsConfiguration = subspace.getPnpmOptions() || rushConfiguration.pnpmOptions; if (!commonPackageJson.pnpm) { @@ -132,7 +132,7 @@ export class InstallHelpers { if (rushConfiguration.npmOptions && rushConfiguration.npmOptions.environmentVariables) { configurationEnvironment = rushConfiguration.npmOptions.environmentVariables; } - } else if (rushConfiguration.packageManager === 'pnpm') { + } else if (rushConfiguration.isPnpm) { if (rushConfiguration.pnpmOptions && rushConfiguration.pnpmOptions.environmentVariables) { configurationEnvironment = rushConfiguration.pnpmOptions.environmentVariables; } diff --git a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts index 5761b45c529..0c016ca3106 100644 --- a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts @@ -332,7 +332,7 @@ export class RushInstallManager extends BaseInstallManager { // with the shrinkwrap file, since these will cause install to fail. if ( shrinkwrapFile && - this.rushConfiguration.packageManager === 'pnpm' && + this.rushConfiguration.isPnpm && this.rushConfiguration.experimentsConfiguration.configuration.usePnpmFrozenLockfileForRushInstall ) { const pnpmShrinkwrapFile: PnpmShrinkwrapFile = shrinkwrapFile as PnpmShrinkwrapFile; @@ -361,7 +361,7 @@ export class RushInstallManager extends BaseInstallManager { } // Remove the workspace file if it exists - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { const workspaceFilePath: string = path.join( this.rushConfiguration.commonTempFolder, 'pnpm-workspace.yaml' @@ -622,7 +622,7 @@ export class RushInstallManager extends BaseInstallManager { }, this.options.maxInstallAttempts, () => { - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { // eslint-disable-next-line no-console console.log(Colorize.yellow(`Deleting the "node_modules" folder`)); this.installRecycler.moveFolder(commonNodeModulesFolder); diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 86bb2e15fd3..768904e0cf3 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -431,7 +431,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { const potentiallyChangedFiles: string[] = []; - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { // Add workspace file. This file is only modified when workspace packages change. const pnpmWorkspaceFilename: string = path.join( subspace.getSubspaceTempFolderPath(), @@ -568,7 +568,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { }, this.options.maxInstallAttempts, () => { - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { this._terminal.writeWarningLine(`Deleting the "node_modules" folder`); this.installRecycler.moveFolder(commonNodeModulesFolder); @@ -649,10 +649,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { }, { concurrency: 10 } ); - } else if ( - this.rushConfiguration.packageManager === 'pnpm' && - this.rushConfiguration.pnpmOptions?.useWorkspaces - ) { + } else if (this.rushConfiguration.isPnpm && this.rushConfiguration.pnpmOptions?.useWorkspaces) { // If we're in PNPM workspace mode and PNPM didn't create a shrinkwrap file, // there are no dependencies. Generate empty shrinkwrap files for all projects. await Async.forEachAsync( @@ -749,7 +746,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { super.pushConfigurationArgs(args, options, subspace); // Add workspace-specific args - if (this.rushConfiguration.packageManager === 'pnpm') { + if (this.rushConfiguration.isPnpm) { args.push('--recursive'); args.push('--link-workspace-packages', 'false'); diff --git a/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts b/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts index ac6acbc2a43..07b06fc0e68 100644 --- a/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts +++ b/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts @@ -110,7 +110,8 @@ export class SetupPackageRegistry { if (!this._options.syncNpmrcAlreadyCalled) { Utilities.syncNpmrc({ sourceNpmrcFolder: this.rushConfiguration.commonRushConfigFolder, - targetNpmrcFolder: this.rushConfiguration.commonTempFolder + targetNpmrcFolder: this.rushConfiguration.commonTempFolder, + supportEnvVarFallbackSyntax: this.rushConfiguration.isPnpm }); } diff --git a/libraries/rush-lib/src/scripts/install-run.ts b/libraries/rush-lib/src/scripts/install-run.ts index f6332c0ebd7..775e87eb89e 100644 --- a/libraries/rush-lib/src/scripts/install-run.ts +++ b/libraries/rush-lib/src/scripts/install-run.ts @@ -169,7 +169,8 @@ function _resolvePackageVersion( syncNpmrc({ sourceNpmrcFolder, targetNpmrcFolder: rushTempFolder, - logger + logger, + supportEnvVarFallbackSyntax: false }); const npmPath: string = getNpmPath(); @@ -433,7 +434,8 @@ export function installAndRun( syncNpmrc({ sourceNpmrcFolder, targetNpmrcFolder: packageInstallFolder, - logger + logger, + supportEnvVarFallbackSyntax: false }); _createPackageJson(packageInstallFolder, packageName, packageVersion); diff --git a/libraries/rush-lib/src/utilities/Utilities.ts b/libraries/rush-lib/src/utilities/Utilities.ts index f4f8901b00c..d521640b543 100644 --- a/libraries/rush-lib/src/utilities/Utilities.ts +++ b/libraries/rush-lib/src/utilities/Utilities.ts @@ -531,7 +531,8 @@ export class Utilities { if (commonRushConfigFolder) { Utilities.syncNpmrc({ sourceNpmrcFolder: commonRushConfigFolder, - targetNpmrcFolder: directory + targetNpmrcFolder: directory, + supportEnvVarFallbackSyntax: false }); } diff --git a/libraries/rush-lib/src/utilities/npmrcUtilities.ts b/libraries/rush-lib/src/utilities/npmrcUtilities.ts index e3bd9026e21..db15a0d17d4 100644 --- a/libraries/rush-lib/src/utilities/npmrcUtilities.ts +++ b/libraries/rush-lib/src/utilities/npmrcUtilities.ts @@ -22,27 +22,55 @@ export interface ILogger { // create a global _combinedNpmrc for cache purpose const _combinedNpmrcMap: Map = new Map(); -function _trimNpmrcFile(options: { - sourceNpmrcPath: string; - linesToPrepend?: string[]; - linesToAppend?: string[]; -}): string { - const { sourceNpmrcPath, linesToPrepend, linesToAppend } = options; +function _trimNpmrcFile( + options: Pick< + INpmrcTrimOptions, + 'sourceNpmrcPath' | 'linesToAppend' | 'linesToPrepend' | 'supportEnvVarFallbackSyntax' + > +): string { + const { sourceNpmrcPath, linesToPrepend, linesToAppend, supportEnvVarFallbackSyntax } = options; const combinedNpmrcFromCache: string | undefined = _combinedNpmrcMap.get(sourceNpmrcPath); if (combinedNpmrcFromCache !== undefined) { return combinedNpmrcFromCache; } + let npmrcFileLines: string[] = []; if (linesToPrepend) { npmrcFileLines.push(...linesToPrepend); } + if (fs.existsSync(sourceNpmrcPath)) { npmrcFileLines.push(...fs.readFileSync(sourceNpmrcPath).toString().split('\n')); } + if (linesToAppend) { npmrcFileLines.push(...linesToAppend); } + npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim()); + + const resultLines: string[] = trimNpmrcFileLines(npmrcFileLines, process.env, supportEnvVarFallbackSyntax); + + const combinedNpmrc: string = resultLines.join('\n'); + + //save the cache + _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc); + + return combinedNpmrc; +} + +/** + * + * @param npmrcFileLines The npmrc file's lines + * @param env The environment variables object + * @param supportEnvVarFallbackSyntax Whether to support fallback values in the form of `${VAR_NAME:-fallback}` + * @returns + */ +export function trimNpmrcFileLines( + npmrcFileLines: string[], + env: NodeJS.ProcessEnv, + supportEnvVarFallbackSyntax: boolean +): string[] { const resultLines: string[] = []; // This finds environment variable tokens that look like "${VAR_NAME}" @@ -66,11 +94,36 @@ function _trimNpmrcFile(options: { const environmentVariables: string[] | null = line.match(expansionRegExp); if (environmentVariables) { for (const token of environmentVariables) { - // Remove the leading "${" and the trailing "}" from the token - const environmentVariableName: string = token.substring(2, token.length - 1); + /** + * Remove the leading "${" and the trailing "}" from the token + * + * ${nameString} -> nameString + * ${nameString-fallbackString} -> name-fallbackString + * ${nameString:-fallbackString} -> name:-fallbackString + */ + const nameWithFallback: string = token.substring(2, token.length - 1); + + let environmentVariableName: string; + let fallback: string | undefined; + if (supportEnvVarFallbackSyntax) { + /** + * Get the environment variable name and fallback value. + * + * name fallback + * nameString -> nameString undefined + * nameString-fallbackString -> nameString fallbackString + * nameString:-fallbackString -> nameString fallbackString + */ + const matched: string[] | null = nameWithFallback.match(/^([^:-]+)(?:\:?-(.+))?$/); + // matched: [originStr, variableName, fallback] + environmentVariableName = matched?.[1] ?? nameWithFallback; + fallback = matched?.[2]; + } else { + environmentVariableName = nameWithFallback; + } - // Is the environment variable defined? - if (!process.env[environmentVariableName]) { + // Is the environment variable and fallback value defined. + if (!env[environmentVariableName] && !fallback) { // No, so trim this line lineShouldBeTrimmed = true; break; @@ -88,12 +141,7 @@ function _trimNpmrcFile(options: { } } - const combinedNpmrc: string = resultLines.join('\n'); - - //save the cache - _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc); - - return combinedNpmrc; + return resultLines; } /** @@ -116,17 +164,15 @@ interface INpmrcTrimOptions { logger: ILogger; linesToPrepend?: string[]; linesToAppend?: string[]; + supportEnvVarFallbackSyntax: boolean; } + function _copyAndTrimNpmrcFile(options: INpmrcTrimOptions): string { - const { logger, sourceNpmrcPath, targetNpmrcPath, linesToPrepend, linesToAppend } = options; + const { logger, sourceNpmrcPath, targetNpmrcPath } = options; logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose logger.info(` --> "${targetNpmrcPath}"`); - const combinedNpmrc: string = _trimNpmrcFile({ - sourceNpmrcPath, - linesToPrepend, - linesToAppend - }); + const combinedNpmrc: string = _trimNpmrcFile(options); fs.writeFileSync(targetNpmrcPath, combinedNpmrc); @@ -145,12 +191,14 @@ function _copyAndTrimNpmrcFile(options: INpmrcTrimOptions): string { export interface ISyncNpmrcOptions { sourceNpmrcFolder: string; targetNpmrcFolder: string; + supportEnvVarFallbackSyntax: boolean; useNpmrcPublish?: boolean; logger?: ILogger; linesToPrepend?: string[]; linesToAppend?: string[]; createIfMissing?: boolean; } + export function syncNpmrc(options: ISyncNpmrcOptions): string | undefined { const { sourceNpmrcFolder, @@ -162,9 +210,7 @@ export function syncNpmrc(options: ISyncNpmrcOptions): string | undefined { // eslint-disable-next-line no-console error: console.error }, - createIfMissing = false, - linesToAppend, - linesToPrepend + createIfMissing = false } = options; const sourceNpmrcPath: string = path.join( sourceNpmrcFolder, @@ -177,12 +223,12 @@ export function syncNpmrc(options: ISyncNpmrcOptions): string | undefined { if (!fs.existsSync(targetNpmrcFolder)) { fs.mkdirSync(targetNpmrcFolder, { recursive: true }); } + return _copyAndTrimNpmrcFile({ sourceNpmrcPath, targetNpmrcPath, logger, - linesToAppend, - linesToPrepend + ...options }); } else if (fs.existsSync(targetNpmrcPath)) { // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target @@ -194,7 +240,11 @@ export function syncNpmrc(options: ISyncNpmrcOptions): string | undefined { } } -export function isVariableSetInNpmrcFile(sourceNpmrcFolder: string, variableKey: string): boolean { +export function isVariableSetInNpmrcFile( + sourceNpmrcFolder: string, + variableKey: string, + supportEnvVarFallbackSyntax: boolean +): boolean { const sourceNpmrcPath: string = `${sourceNpmrcFolder}/.npmrc`; //if .npmrc file does not exist, return false directly @@ -202,7 +252,7 @@ export function isVariableSetInNpmrcFile(sourceNpmrcFolder: string, variableKey: return false; } - const trimmedNpmrcFile: string = _trimNpmrcFile({ sourceNpmrcPath }); + const trimmedNpmrcFile: string = _trimNpmrcFile({ sourceNpmrcPath, supportEnvVarFallbackSyntax }); const variableKeyRegExp: RegExp = new RegExp(`^${variableKey}=`, 'm'); return trimmedNpmrcFile.match(variableKeyRegExp) !== null; diff --git a/libraries/rush-lib/src/utilities/test/__snapshots__/npmrcUtilities.test.ts.snap b/libraries/rush-lib/src/utilities/test/__snapshots__/npmrcUtilities.test.ts.snap new file mode 100644 index 00000000000..691864176a2 --- /dev/null +++ b/libraries/rush-lib/src/utilities/test/__snapshots__/npmrcUtilities.test.ts.snap @@ -0,0 +1,225 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a a variable without a fallback 1`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a a variable without a fallback 2`] = ` +Array [ + "var1=\${foo}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a variable with a fallback 1`] = ` +Array [ + "var1=\${foo-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a variable with a fallback 2`] = ` +Array [ + "var1=\${foo-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a variable with a fallback 3`] = ` +Array [ + "var1=\${foo:-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a variable with a fallback 4`] = ` +Array [ + "var1=\${foo:-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a variable with a fallback 5`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo}-\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a variable with a fallback 6`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo}-\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a variable with a fallback 7`] = ` +Array [ + "var1=\${foo}-\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports a variable with a fallback 8`] = ` +Array [ + "var1=\${foo:-fallback_value}-\${bar-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports malformed lines 1`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo_fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports malformed lines 2`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo:fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports malformed lines 3`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo:_fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports malformed lines 4`] = ` +Array [ + "var1=\${foo", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports multiple lines 1`] = ` +Array [ + "var1=\${foo}", + "; MISSING ENVIRONMENT VARIABLE: var2=\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports multiple lines 2`] = ` +Array [ + "var1=\${foo}", + "var2=\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports multiple lines 3`] = ` +Array [ + "var1=\${foo}", + "var2=\${bar-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines With support for env var fallback syntax supports multiple lines 4`] = ` +Array [ + "var1=\${foo:-fallback_value}", + "var2=\${bar-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a a variable without a fallback 1`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a a variable without a fallback 2`] = ` +Array [ + "var1=\${foo}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a variable with a fallback 1`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a variable with a fallback 2`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a variable with a fallback 3`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo:-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a variable with a fallback 4`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo:-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a variable with a fallback 5`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo}-\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a variable with a fallback 6`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo}-\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a variable with a fallback 7`] = ` +Array [ + "var1=\${foo}-\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports a variable with a fallback 8`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo:-fallback_value}-\${bar-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports malformed lines 1`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo_fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports malformed lines 2`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo:fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports malformed lines 3`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo:_fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports malformed lines 4`] = ` +Array [ + "var1=\${foo", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports multiple lines 1`] = ` +Array [ + "var1=\${foo}", + "; MISSING ENVIRONMENT VARIABLE: var2=\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports multiple lines 2`] = ` +Array [ + "var1=\${foo}", + "var2=\${bar}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports multiple lines 3`] = ` +Array [ + "var1=\${foo}", + "; MISSING ENVIRONMENT VARIABLE: var2=\${bar-fallback_value}", +] +`; + +exports[`npmrcUtilities trimNpmrcFileLines Without support for env var fallback syntax supports multiple lines 4`] = ` +Array [ + "; MISSING ENVIRONMENT VARIABLE: var1=\${foo:-fallback_value}", + "; MISSING ENVIRONMENT VARIABLE: var2=\${bar-fallback_value}", +] +`; diff --git a/libraries/rush-lib/src/utilities/test/npmrcUtilities.test.ts b/libraries/rush-lib/src/utilities/test/npmrcUtilities.test.ts new file mode 100644 index 00000000000..b889435a0f7 --- /dev/null +++ b/libraries/rush-lib/src/utilities/test/npmrcUtilities.test.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { trimNpmrcFileLines } from '../npmrcUtilities'; + +describe('npmrcUtilities', () => { + function runTests(supportEnvVarFallbackSyntax: boolean): void { + it('handles empty input', () => { + expect(trimNpmrcFileLines([], {}, supportEnvVarFallbackSyntax)).toEqual([]); + }); + + it('supports a a variable without a fallback', () => { + expect(trimNpmrcFileLines(['var1=${foo}'], {}, supportEnvVarFallbackSyntax)).toMatchSnapshot(); + expect( + trimNpmrcFileLines(['var1=${foo}'], { foo: 'test' }, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + }); + + it('supports a variable with a fallback', () => { + expect( + trimNpmrcFileLines(['var1=${foo-fallback_value}'], {}, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines(['var1=${foo-fallback_value}'], { foo: 'test' }, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines(['var1=${foo:-fallback_value}'], {}, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines(['var1=${foo:-fallback_value}'], { foo: 'test' }, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines(['var1=${foo}-${bar}'], { foo: 'test' }, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines(['var1=${foo}-${bar}'], { bar: 'test' }, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines(['var1=${foo}-${bar}'], { foo: 'test', bar: 'test' }, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines( + ['var1=${foo:-fallback_value}-${bar-fallback_value}'], + {}, + supportEnvVarFallbackSyntax + ) + ).toMatchSnapshot(); + }); + + it('supports multiple lines', () => { + expect( + trimNpmrcFileLines(['var1=${foo}', 'var2=${bar}'], { foo: 'test' }, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines( + ['var1=${foo}', 'var2=${bar}'], + { foo: 'test', bar: 'test' }, + supportEnvVarFallbackSyntax + ) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines( + ['var1=${foo}', 'var2=${bar-fallback_value}'], + { foo: 'test' }, + supportEnvVarFallbackSyntax + ) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines( + ['var1=${foo:-fallback_value}', 'var2=${bar-fallback_value}'], + {}, + supportEnvVarFallbackSyntax + ) + ).toMatchSnapshot(); + }); + + it('supports malformed lines', () => { + // Malformed + expect( + trimNpmrcFileLines(['var1=${foo_fallback_value}'], {}, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines(['var1=${foo:fallback_value}'], {}, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect( + trimNpmrcFileLines(['var1=${foo:_fallback_value}'], {}, supportEnvVarFallbackSyntax) + ).toMatchSnapshot(); + expect(trimNpmrcFileLines(['var1=${foo'], {}, supportEnvVarFallbackSyntax)).toMatchSnapshot(); + }); + } + + describe(trimNpmrcFileLines.name, () => { + describe('With support for env var fallback syntax', () => runTests(true)); + describe('Without support for env var fallback syntax', () => runTests(false)); + }); +});