@@ -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
0 commit comments