Skip to content

Commit 3e3a340

Browse files
committed
All package manager installations now use a standard implementation for copying .npmrc and stripping environment variables
1 parent 3ec8753 commit 3e3a340

6 files changed

Lines changed: 133 additions & 39 deletions

File tree

apps/rush-lib/src/logic/InstallManager.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,14 @@ export class InstallManager {
280280
console.log(colors.bold(`Installing ${packageManager} version ${packageManagerVersion}${os.EOL}`));
281281

282282
// note that this will remove the last-install flag from the directory
283-
Utilities.installPackageInDirectory(
284-
packageManagerToolFolder,
285-
packageManager,
286-
this._rushConfiguration.packageManagerToolVersion,
287-
`${packageManager}-local-install`,
288-
MAX_INSTALL_ATTEMPTS
289-
);
283+
Utilities.installPackageInDirectory({
284+
directory: packageManagerToolFolder,
285+
packageName: packageManager,
286+
version: this._rushConfiguration.packageManagerToolVersion,
287+
tempPackageTitle: `${packageManager}-local-install`,
288+
maxInstallAttempts: MAX_INSTALL_ATTEMPTS,
289+
commonRushConfigFolder: this._rushConfiguration.commonRushConfigFolder
290+
});
290291

291292
console.log(`Successfully installed ${packageManager} version ${packageManagerVersion}`);
292293
} else {

apps/rush-lib/src/scripts/install-run.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ function resolvePackageVersion(rushCommonFolder: string, { name, version }: IPac
6363
// version resolves to
6464
try {
6565
const rushTempFolder: string = ensureAndJoinPath(rushCommonFolder, 'temp');
66-
syncNpmrc(rushCommonFolder, rushTempFolder);
66+
const sourceNpmrcFolder: string = path.join(rushCommonFolder, 'config', 'rush');
67+
68+
syncNpmrc(sourceNpmrcFolder, rushTempFolder);
69+
6770
const npmPath: string = getNpmPath();
6871

6972
// This returns something that looks like:
@@ -187,17 +190,18 @@ function ensureAndJoinPath(baseFolder: string, ...pathSegments: string[]): strin
187190
return joinedPath;
188191
}
189192

190-
function syncNpmrc(rushCommonFolder: string, targetFolder: string): void {
191-
const npmrcPath: string = path.join(rushCommonFolder, 'config', 'rush', '.npmrc');
192-
const targetNpmrcPath: string = path.join(targetFolder, '.npmrc');
193+
function syncNpmrc(sourceNpmrcFolder: string, targetNpmrcFolder: string): void {
194+
const sourceNpmrcPath: string = path.join(sourceNpmrcFolder, '.npmrc');
195+
const targetNpmrcPath: string = path.join(targetNpmrcFolder, '.npmrc');
193196
try {
194-
if (fs.existsSync(npmrcPath)) {
195-
let npmrcFileLines: string[] = fs.readFileSync(npmrcPath).toString().split('\n');
197+
if (fs.existsSync(sourceNpmrcPath)) {
198+
let npmrcFileLines: string[] = fs.readFileSync(sourceNpmrcPath).toString().split('\n');
196199
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
197200
const resultLines: string[] = [];
198201
// Trim out lines that reference environment variables that aren't defined
199202
for (const line of npmrcFileLines) {
200-
const regex: RegExp = /\$\{([^\}]+)\}/g; // This finds environment variable tokens that look like "${VAR_NAME}"
203+
// This finds environment variable tokens that look like "${VAR_NAME}"
204+
const regex: RegExp = /\$\{([^\}]+)\}/g;
201205
const environmentVariables: string[] | null = line.match(regex);
202206
let lineShouldBeTrimmed: boolean = false;
203207
if (environmentVariables) {
@@ -362,7 +366,10 @@ export function installAndRun(
362366
if (!isPackageAlreadyInstalled(packageInstallFolder)) {
363367
// The package isn't already installed
364368
cleanInstallFolder(rushCommonFolder, packageInstallFolder);
365-
syncNpmrc(rushCommonFolder, packageInstallFolder);
369+
370+
const sourceNpmrcFolder: string = path.join(rushCommonFolder, 'config', 'rush');
371+
syncNpmrc(sourceNpmrcFolder, packageInstallFolder);
372+
366373
createPackageJson(packageInstallFolder, packageName, packageVersion);
367374
installPackage(packageInstallFolder, packageName, packageVersion);
368375
writeFlagFile(packageInstallFolder);

apps/rush-lib/src/utilities/Utilities.ts

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ export interface IEnvironment {
1818
[environmentVariableName: string]: string | undefined;
1919
}
2020

21+
/**
22+
* Options for Utilities.installPackageInDirectory().
23+
*/
24+
export interface IInstallPackageInDirectoryOptions {
25+
directory: string;
26+
packageName: string;
27+
version: string;
28+
tempPackageTitle: string;
29+
maxInstallAttempts: number;
30+
commonRushConfigFolder: string | undefined;
31+
suppressOutput?: boolean;
32+
}
33+
2134
/**
2235
* @public
2336
*/
@@ -390,36 +403,34 @@ export class Utilities {
390403
/**
391404
* Installs a package by name and version in the specified directory.
392405
*/
393-
public static installPackageInDirectory(
394-
directory: string,
395-
packageName: string,
396-
version: string,
397-
tempPackageTitle: string,
398-
maxInstallAttempts: number,
399-
suppressOutput: boolean = false
400-
): void {
406+
public static installPackageInDirectory(options: IInstallPackageInDirectoryOptions): void {
407+
const directory: string = path.resolve(options.directory);
401408
if (fsx.existsSync(directory)) {
402409
console.log('Deleting old files from ' + directory);
403410
}
404411
fsx.emptyDirSync(directory);
405412

406413
const npmPackageJson: IPackageJson = {
407414
dependencies: {
408-
[packageName]: version
415+
[options.packageName]: options.version
409416
},
410417
description: 'Temporary file generated by the Rush tool',
411-
name: tempPackageTitle,
418+
name: options.tempPackageTitle,
412419
private: true,
413420
version: '0.0.0'
414421
};
415422
JsonFile.save(npmPackageJson, path.join(directory, 'package.json'));
416423

424+
if (options.commonRushConfigFolder) {
425+
Utilities._syncNpmrc(options.commonRushConfigFolder, directory);
426+
}
427+
417428
console.log(os.EOL + 'Running "npm install" in ' + directory);
418429

419430
// NOTE: Here we use whatever version of NPM we happen to find in the PATH
420-
Utilities.executeCommandWithRetry(maxInstallAttempts, 'npm', ['install'], directory,
431+
Utilities.executeCommandWithRetry(options.maxInstallAttempts, 'npm', ['install'], directory,
421432
Utilities._createEnvironmentForRushCommand(''),
422-
suppressOutput);
433+
options.suppressOutput);
423434
}
424435

425436
public static withFinally<T>(options: { promise: Promise<T>, finally: () => void }): Promise<T> {
@@ -442,6 +453,59 @@ export class Utilities {
442453
});
443454
}
444455

456+
/**
457+
* NPM allows environment variables to be specified in its .npmrc file. This can be
458+
* used to provide an authentication token for a registry. However if the environment variable
459+
* is undefined, it expands to an empty string, which produces a valid-looking mapping
460+
* with an invalid URL. As a workaround, _syncNpmrc() copies the .npmrc file to the
461+
* target folder and strips out any lines that reference undefined environment variables.
462+
* If the target folder does not exist, then _syncNpmrc() will delete an .npmrc that
463+
* is found there.
464+
*
465+
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH _syncNpmrc() FROM scripts/install-run.ts
466+
*/
467+
private static _syncNpmrc(sourceNpmrcFolder: string, targetNpmrcFolder: string): void {
468+
const sourceNpmrcPath: string = path.join(sourceNpmrcFolder, '.npmrc');
469+
const targetNpmrcPath: string = path.join(targetNpmrcFolder, '.npmrc');
470+
try {
471+
if (fsx.existsSync(sourceNpmrcPath)) {
472+
console.log(`Copying ${sourceNpmrcPath} --> ${targetNpmrcPath}`);
473+
let npmrcFileLines: string[] = fsx.readFileSync(sourceNpmrcPath).toString().split('\n');
474+
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
475+
const resultLines: string[] = [];
476+
// Trim out lines that reference environment variables that aren't defined
477+
for (const line of npmrcFileLines) {
478+
// This finds environment variable tokens that look like "${VAR_NAME}"
479+
const regex: RegExp = /\$\{([^\}]+)\}/g;
480+
const environmentVariables: string[] | null = line.match(regex);
481+
let lineShouldBeTrimmed: boolean = false;
482+
if (environmentVariables) {
483+
for (const token of environmentVariables) {
484+
// Remove the leading "${" and the trailing "}" from the token
485+
const environmentVariableName: string = token.substring(2, token.length - 1);
486+
if (!process.env[environmentVariableName]) {
487+
lineShouldBeTrimmed = true;
488+
break;
489+
}
490+
}
491+
}
492+
493+
if (!lineShouldBeTrimmed) {
494+
resultLines.push(line);
495+
}
496+
}
497+
498+
fsx.writeFileSync(targetNpmrcPath, resultLines.join(os.EOL));
499+
} else if (fsx.existsSync(targetNpmrcPath)) {
500+
// If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
501+
console.log(`Deleting ${targetNpmrcPath}`);
502+
fsx.unlinkSync(targetNpmrcPath);
503+
}
504+
} catch (e) {
505+
throw new Error(`Error syncing .npmrc file: ${e}`);
506+
}
507+
}
508+
445509
/**
446510
* Returns a process.env environment suitable for executing lifecycle scripts.
447511
* @param initCwd - The INIT_CWD environment variable

apps/rush/src/MinimalRushConfiguration.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4+
import * as path from 'path';
5+
46
import { JsonFile } from '@microsoft/node-core-library';
57
import { RushConfiguration } from '@microsoft/rush-lib';
68

@@ -14,7 +16,9 @@ interface IMinimalRushConfigurationJson {
1416
* decide which version of Rush should be installed/used.
1517
*/
1618
export class MinimalRushConfiguration {
19+
private _rushJsonFilename: string;
1720
private _rushVersion: string;
21+
private _commonRushConfigFolder: string;
1822

1923
public static loadFromDefaultLocation(): MinimalRushConfiguration | undefined {
2024
const rushJsonLocation: string | undefined = RushConfiguration.tryFindRushJsonLocation();
@@ -28,14 +32,16 @@ export class MinimalRushConfiguration {
2832
private static _loadFromConfigurationFile(rushJsonFilename: string): MinimalRushConfiguration | undefined {
2933
try {
3034
const minimalRushConfigurationJson: IMinimalRushConfigurationJson = JsonFile.load(rushJsonFilename);
31-
return new MinimalRushConfiguration(minimalRushConfigurationJson);
35+
return new MinimalRushConfiguration(minimalRushConfigurationJson, rushJsonFilename);
3236
} catch (e) {
3337
return undefined;
3438
}
3539
}
3640

37-
private constructor(minimalRushConfigurationJson: IMinimalRushConfigurationJson) {
41+
private constructor(minimalRushConfigurationJson: IMinimalRushConfigurationJson, rushJsonFilename: string) {
3842
this._rushVersion = minimalRushConfigurationJson.rushVersion || minimalRushConfigurationJson.rushMinimumVersion;
43+
this._rushJsonFilename = rushJsonFilename;
44+
this._commonRushConfigFolder = path.join(path.dirname(rushJsonFilename), 'common/config/rush');
3945
}
4046

4147
/**
@@ -46,4 +52,16 @@ export class MinimalRushConfiguration {
4652
public get rushVersion(): string {
4753
return this._rushVersion;
4854
}
55+
56+
/**
57+
* The folder where Rush's additional config files are stored. This folder is always a
58+
* subfolder called "config\rush" inside the common folder. (The "common\config" folder
59+
* is reserved for configuration files used by other tools.) To avoid confusion or mistakes,
60+
* Rush will report an error if this this folder contains any unrecognized files.
61+
*
62+
* Example: "C:\MyRepo\common\config\rush"
63+
*/
64+
public get commonRushConfigFolder(): string {
65+
return this._commonRushConfigFolder;
66+
}
4967
}

apps/rush/src/RushVersionSelector.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { LockFile } from '@microsoft/node-core-library';
88
import { Utilities } from '@microsoft/rush-lib/lib/utilities/Utilities';
99
import { _LastInstallFlag } from '@microsoft/rush-lib';
1010
import { RushCommandSelector } from './RushCommandSelector';
11+
import { MinimalRushConfiguration } from './MinimalRushConfiguration';
1112

1213
const MAX_INSTALL_ATTEMPTS: number = 3;
1314

@@ -20,7 +21,9 @@ export class RushVersionSelector {
2021
this._currentPackageVersion = currentPackageVersion;
2122
}
2223

23-
public ensureRushVersionInstalled(version: string): Promise<void> {
24+
public ensureRushVersionInstalled(version: string,
25+
configuration: MinimalRushConfiguration | undefined): Promise<void> {
26+
2427
const isLegacyRushVersion: boolean = semver.lt(version, '4.0.0');
2528
const expectedRushPath: string = path.join(this._rushDirectory, `rush-${version}`);
2629

@@ -46,14 +49,15 @@ export class RushVersionSelector {
4649
if (installMarker.isValid()) {
4750
console.log('Another process performed the installation.');
4851
} else {
49-
Utilities.installPackageInDirectory(
50-
expectedRushPath,
51-
isLegacyRushVersion ? '@microsoft/rush' : '@microsoft/rush-lib',
52-
version,
53-
'rush-local-install',
54-
MAX_INSTALL_ATTEMPTS,
55-
true
56-
);
52+
Utilities.installPackageInDirectory({
53+
directory: expectedRushPath,
54+
packageName: isLegacyRushVersion ? '@microsoft/rush' : '@microsoft/rush-lib',
55+
version: version,
56+
tempPackageTitle: 'rush-local-install',
57+
maxInstallAttempts: MAX_INSTALL_ATTEMPTS,
58+
commonRushConfigFolder: configuration ? configuration.commonRushConfigFolder : undefined,
59+
suppressOutput: true
60+
});
5761

5862
console.log(`Successfully installed Rush version ${version} in ${expectedRushPath}.`);
5963

apps/rush/src/start.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ if (rushVersionToLoad && semver.lt(rushVersionToLoad, '5.0.0-dev.18')) {
9898
// install it
9999
if (rushVersionToLoad && rushVersionToLoad !== currentPackageJson.version) {
100100
const versionSelector: RushVersionSelector = new RushVersionSelector(currentPackageJson.version);
101-
versionSelector.ensureRushVersionInstalled(rushVersionToLoad)
101+
versionSelector.ensureRushVersionInstalled(rushVersionToLoad, configuration)
102102
.catch((error: Error) => {
103103
console.log(colors.red('Error: ' + error.message));
104104
});

0 commit comments

Comments
 (0)