2017-10-06 05:48:18 -04:00
'use strict' ;
const chalk = require ( 'chalk' ) ;
const fs = require ( 'fs-extra' ) ;
2019-02-04 09:45:45 -05:00
const lockfile = require ( '@yarnpkg/lockfile' ) ;
2017-10-06 05:48:18 -04:00
const path = require ( 'canonical-path' ) ;
2019-02-04 09:45:45 -05:00
const semver = require ( 'semver' ) ;
2017-10-06 05:48:18 -04:00
const shelljs = require ( 'shelljs' ) ;
const yargs = require ( 'yargs' ) ;
const PACKAGE _JSON = 'package.json' ;
2019-02-04 09:45:45 -05:00
const YARN _LOCK = 'yarn.lock' ;
2017-10-06 05:48:18 -04:00
const LOCAL _MARKER _PATH = 'node_modules/_local_.json' ;
const PACKAGE _JSON _REGEX = /^[^/]+\/package\.json$/ ;
const ANGULAR _ROOT _DIR = path . resolve ( _ _dirname , '../../..' ) ;
2019-08-03 10:37:19 -04:00
const ANGULAR _DIST _PACKAGES = path . join ( ANGULAR _ROOT _DIR , 'dist/packages-dist' ) ;
const ANGULAR _DIST _PACKAGES _BUILD _CMD = path . join ( ANGULAR _ROOT _DIR , 'scripts/build-packages-dist.sh' ) ;
2017-10-06 05:48:18 -04:00
/ * *
* A tool that can install Angular dependencies for a project from NPM or from the
* locally built distributables .
*
* This tool is used to change dependencies of the ` aio ` application and the example
* applications .
* /
class NgPackagesInstaller {
/ * *
* Create a new installer for a project in the specified directory .
*
* @ param { string } projectDir - the path to the directory containing the project .
2019-08-03 09:57:24 -04:00
* @ param { object } options - a hash of options for the install :
* * ` debug ` ( ` boolean ` ) - whether to display debug messages .
* * ` force ` ( ` boolean ` ) - whether to force a local installation even if there is a local marker file .
2019-08-03 10:37:19 -04:00
* * ` buildPackages ` ( ` boolean ` ) - whether to build the local Angular packages before using them .
* ( NOTE : Building the packages is currently not supported on Windows , so a message is printed instead . )
2019-08-03 09:57:24 -04:00
* * ` ignorePackages ` ( ` string[] ` ) - a collection of names of packages that should not be copied over .
2017-10-06 05:48:18 -04:00
* /
constructor ( projectDir , options = { } ) {
2019-08-03 11:14:27 -04:00
this . debug = this . _parseBooleanArg ( options . debug ) ;
this . force = this . _parseBooleanArg ( options . force ) ;
this . buildPackages = this . _parseBooleanArg ( options . buildPackages ) ;
2017-10-06 05:48:32 -04:00
this . ignorePackages = options . ignorePackages || [ ] ;
2017-10-06 05:48:18 -04:00
this . projectDir = path . resolve ( projectDir ) ;
this . localMarkerPath = path . resolve ( this . projectDir , LOCAL _MARKER _PATH ) ;
this . _log ( 'Project directory:' , this . projectDir ) ;
}
// Public methods
/ * *
* Check whether the dependencies have been overridden with locally built
* Angular packages . This is done by checking for the ` _local_.json ` marker file .
* This will emit a warning to the console if the dependencies have been overridden .
* /
checkDependencies ( ) {
if ( this . _checkLocalMarker ( ) ) {
this . _printWarning ( ) ;
}
}
/ * *
* Install locally built Angular dependencies , overriding the dependencies in the package . json
* This will also write a "marker" file ( ` _local_.json ` ) , which contains the overridden package . json
* contents and acts as an indicator that dependencies have been overridden .
* /
installLocalDependencies ( ) {
2019-02-04 07:55:58 -05:00
if ( this . force || ! this . _checkLocalMarker ( ) ) {
2017-10-06 05:48:18 -04:00
const pathToPackageConfig = path . resolve ( this . projectDir , PACKAGE _JSON ) ;
2019-02-04 09:45:45 -05:00
const pathToLockfile = path . resolve ( this . projectDir , YARN _LOCK ) ;
const parsedLockfile = this . _parseLockfile ( pathToLockfile ) ;
2017-10-06 05:48:18 -04:00
const packages = this . _getDistPackages ( ) ;
2017-10-09 06:11:13 -04:00
try {
// Overwrite local Angular packages dependencies to other Angular packages with local files.
Object . keys ( packages ) . forEach ( key => {
const pkg = packages [ key ] ;
const tmpConfig = JSON . parse ( JSON . stringify ( pkg . config ) ) ;
// Prevent accidental publishing of the package, if something goes wrong.
tmpConfig . private = true ;
// Overwrite project dependencies/devDependencies to Angular packages with local files.
[ 'dependencies' , 'devDependencies' ] . forEach ( prop => {
const deps = tmpConfig [ prop ] || { } ;
Object . keys ( deps ) . forEach ( key2 => {
const pkg2 = packages [ key2 ] ;
if ( pkg2 ) {
// point the core Angular packages at the distributable folder
deps [ key2 ] = ` file: ${ pkg2 . parentDir } / ${ key2 . replace ( '@angular/' , '' ) } ` ;
this . _log ( ` Overriding dependency of local ${ key } with local package: ${ key2 } : ${ deps [ key2 ] } ` ) ;
}
} ) ;
} ) ;
2019-02-04 07:31:43 -05:00
fs . writeFileSync ( pkg . packageJsonPath , JSON . stringify ( tmpConfig , null , 2 ) ) ;
2017-10-09 06:11:13 -04:00
} ) ;
2017-10-06 05:48:18 -04:00
2019-02-04 07:31:43 -05:00
const packageConfigFile = fs . readFileSync ( pathToPackageConfig , 'utf8' ) ;
2017-10-09 06:11:13 -04:00
const packageConfig = JSON . parse ( packageConfigFile ) ;
2017-10-06 05:48:18 -04:00
2017-10-09 06:11:13 -04:00
const [ dependencies , peers ] = this . _collectDependencies ( packageConfig . dependencies || { } , packages ) ;
const [ devDependencies , devPeers ] = this . _collectDependencies ( packageConfig . devDependencies || { } , packages ) ;
2017-10-06 05:48:18 -04:00
2019-02-04 09:45:45 -05:00
this . _assignPeerDependencies ( peers , dependencies , devDependencies , parsedLockfile ) ;
this . _assignPeerDependencies ( devPeers , dependencies , devDependencies , parsedLockfile ) ;
2017-10-09 06:11:13 -04:00
const localPackageConfig = Object . assign ( Object . create ( null ) , packageConfig , { dependencies , devDependencies } ) ;
localPackageConfig . _ _angular = { local : true } ;
const localPackageConfigJson = JSON . stringify ( localPackageConfig , null , 2 ) ;
try {
this . _log ( ` Writing temporary local ${ PACKAGE _JSON } to ${ pathToPackageConfig } ` ) ;
fs . writeFileSync ( pathToPackageConfig , localPackageConfigJson ) ;
2019-02-04 08:03:25 -05:00
this . _installDeps ( '--pure-lockfile' , '--check-files' ) ;
2017-10-09 06:11:13 -04:00
this . _setLocalMarker ( localPackageConfigJson ) ;
} finally {
this . _log ( ` Restoring original ${ PACKAGE _JSON } to ${ pathToPackageConfig } ` ) ;
fs . writeFileSync ( pathToPackageConfig , packageConfigFile ) ;
}
2017-10-06 05:48:18 -04:00
} finally {
2017-10-09 06:11:13 -04:00
// Restore local Angular packages dependencies to other Angular packages.
this . _log ( ` Restoring original ${ PACKAGE _JSON } for local Angular packages. ` ) ;
Object . keys ( packages ) . forEach ( key => {
const pkg = packages [ key ] ;
2019-02-04 07:31:43 -05:00
fs . writeFileSync ( pkg . packageJsonPath , JSON . stringify ( pkg . config , null , 2 ) ) ;
2017-10-09 06:11:13 -04:00
} ) ;
2017-10-06 05:48:18 -04:00
}
}
}
/ * *
* Reinstall the original package . json depdendencies
* Yarn will also delete the local marker file for us .
* /
restoreNpmDependencies ( ) {
2017-10-19 05:19:45 -04:00
this . _installDeps ( '--frozen-lockfile' , '--check-files' ) ;
2017-10-06 05:48:18 -04:00
}
// Protected helpers
2019-02-04 09:45:45 -05:00
_assignPeerDependencies ( peerDependencies , dependencies , devDependencies , parsedLockfile ) {
2017-10-06 05:48:18 -04:00
Object . keys ( peerDependencies ) . forEach ( key => {
2019-02-04 09:45:45 -05:00
const peerDepRange = peerDependencies [ key ] ;
// Ignore peerDependencies whose range is already satisfied by current version in lockfile.
const originalRange = dependencies [ key ] || devDependencies [ key ] ;
const lockfileVersion = originalRange && parsedLockfile [ ` ${ key } @ ${ originalRange } ` ] . version ;
if ( lockfileVersion && semver . satisfies ( lockfileVersion , peerDepRange ) ) return ;
2017-10-06 05:48:18 -04:00
// If there is already an equivalent dependency then override it - otherwise assign/override the devDependency
if ( dependencies [ key ] ) {
2019-02-04 09:45:45 -05:00
this . _log ( ` Overriding dependency with peerDependency: ${ key } : ${ peerDepRange } ` ) ;
dependencies [ key ] = peerDepRange ;
2017-10-06 05:48:18 -04:00
} else {
2019-02-04 09:45:45 -05:00
this . _log ( ` ${ devDependencies [ key ] ? 'Overriding' : 'Assigning' } devDependency with peerDependency: ${ key } : ${ peerDepRange } ` ) ;
devDependencies [ key ] = peerDepRange ;
2017-10-06 05:48:18 -04:00
}
} ) ;
}
2019-08-03 10:37:19 -04:00
/ * *
* Build the local Angular packages .
*
* NOTE :
* Building the packages is currently not supported on Windows , so a message is printed instead , prompting the user to
* do it themselves ( e . g . using Windows Subsystem for Linux or a docker container ) .
* /
_buildDistPackages ( ) {
const canBuild = process . platform !== 'win32' ;
if ( canBuild ) {
this . _log ( ` Building the Angular packages with: ${ ANGULAR _DIST _PACKAGES _BUILD _CMD } ` ) ;
shelljs . exec ( ANGULAR _DIST _PACKAGES _BUILD _CMD ) ;
} else {
this . _warn ( [
'Automatically building the local Angular packages is currently not supported on Windows.' ,
` Please, ensure ' ${ ANGULAR _DIST _PACKAGES } ' exists and is up-to-date (e.g. by running ` +
` ' ${ ANGULAR _DIST _PACKAGES _BUILD _CMD } ' in Git Bash for Windows, Windows Subsystem for Linux or a Linux ` +
'docker container or VM).' ,
'' ,
'Proceeding anyway...' ,
] . join ( '\n' ) ) ;
}
}
2017-10-06 05:48:18 -04:00
_collectDependencies ( dependencies , packages ) {
const peerDependencies = Object . create ( null ) ;
const mergedDependencies = Object . assign ( Object . create ( null ) , dependencies ) ;
Object . keys ( dependencies ) . forEach ( key => {
const sourcePackage = packages [ key ] ;
if ( sourcePackage ) {
// point the core Angular packages at the distributable folder
2017-10-06 17:25:27 -04:00
mergedDependencies [ key ] = ` file: ${ sourcePackage . parentDir } / ${ key . replace ( '@angular/' , '' ) } ` ;
2017-10-06 05:48:18 -04:00
this . _log ( ` Overriding dependency with local package: ${ key } : ${ mergedDependencies [ key ] } ` ) ;
// grab peer dependencies
2017-10-06 17:25:27 -04:00
const sourcePackagePeerDeps = sourcePackage . config . peerDependencies || { } ;
Object . keys ( sourcePackagePeerDeps )
2017-10-06 05:48:18 -04:00
// ignore peerDependencies which are already core Angular packages
. filter ( key => ! packages [ key ] )
2017-10-06 17:25:27 -04:00
. forEach ( key => peerDependencies [ key ] = sourcePackagePeerDeps [ key ] ) ;
2017-10-06 05:48:18 -04:00
}
} ) ;
2018-03-06 12:01:25 -05:00
2017-10-06 05:48:18 -04:00
return [ mergedDependencies , peerDependencies ] ;
}
/ * *
* A hash of Angular package configs .
2019-08-03 09:57:24 -04:00
* ( Detected as directories in '/dist/packages-dist/' that contain a top - level 'package.json' file . )
2017-10-06 05:48:18 -04:00
* /
_getDistPackages ( ) {
const packageConfigs = Object . create ( null ) ;
2019-08-03 09:57:24 -04:00
const distDir = ANGULAR _DIST _PACKAGES ;
this . _log ( ` Angular distributable directory: ${ distDir } . ` ) ;
2019-08-03 10:37:19 -04:00
if ( this . buildPackages ) {
this . _buildDistPackages ( ) ;
}
2019-08-03 09:57:24 -04:00
shelljs
. find ( distDir )
. map ( filePath => filePath . slice ( distDir . length + 1 ) )
. filter ( filePath => PACKAGE _JSON _REGEX . test ( filePath ) )
. forEach ( packagePath => {
const packageName = ` @angular/ ${ packagePath . slice ( 0 , - PACKAGE _JSON . length - 1 ) } ` ;
if ( this . ignorePackages . indexOf ( packageName ) === - 1 ) {
const packageConfig = require ( path . resolve ( distDir , packagePath ) ) ;
packageConfigs [ packageName ] = {
parentDir : distDir ,
packageJsonPath : path . resolve ( distDir , packagePath ) ,
config : packageConfig
} ;
} else {
this . _log ( 'Ignoring package' , packageName ) ;
}
} ) ;
2017-10-06 17:25:27 -04:00
2017-10-06 05:48:18 -04:00
this . _log ( 'Found the following Angular distributables:' , Object . keys ( packageConfigs ) . map ( key => ` \n - ${ key } ` ) ) ;
return packageConfigs ;
}
_installDeps ( ... options ) {
const command = 'yarn install ' + options . join ( ' ' ) ;
this . _log ( 'Installing dependencies with:' , command ) ;
shelljs . exec ( command , { cwd : this . projectDir } ) ;
}
/ * *
* Log a message if the ` debug ` property is set to true .
* @ param { ... string [ ] } messages - The messages to be logged .
* /
_log ( ... messages ) {
if ( this . debug ) {
const header = ` [ ${ NgPackagesInstaller . name } ]: ` ;
const indent = ' ' . repeat ( header . length ) ;
const message = messages . join ( ' ' ) ;
console . info ( ` ${ header } ${ message . split ( '\n' ) . join ( ` \n ${ indent } ` ) } ` ) ;
}
}
2019-08-03 11:14:27 -04:00
/ * *
* Extract the value for a boolean cli argument / option . When passing an option multiple times , ` yargs ` parses it as an
* array of boolean values . In that case , we only care about the last occurrence .
*
* This can be useful , for example , when one has a base command with the option turned on and another command
* ( building on top of the first one ) turning the option off :
* ` ` `
* "base-command" : "my-script --foo --bar" ,
* "no-bar-command" : "yarn base-command --no-bar" ,
* ` ` `
* /
_parseBooleanArg ( value ) {
return Array . isArray ( value ) ? value . pop ( ) : value ;
}
2019-02-04 09:45:45 -05:00
/ * *
* Parse and return a ` yarn.lock ` file .
* /
_parseLockfile ( lockfilePath ) {
const lockfileContent = fs . readFileSync ( lockfilePath , 'utf8' ) ;
const parsed = lockfile . parse ( lockfileContent ) ;
if ( parsed . type !== 'success' ) {
throw new Error ( ` [ ${ NgPackagesInstaller . name } ]: Error parsing lockfile ' ${ lockfilePath } ' (result type: ${ parsed . type } ). ` ) ;
}
return parsed . object ;
}
2017-10-06 05:48:18 -04:00
_printWarning ( ) {
const relativeScriptPath = path . relative ( '.' , _ _filename . replace ( /\.js$/ , '' ) ) ;
const absoluteProjectDir = path . resolve ( this . projectDir ) ;
const restoreCmd = ` node ${ relativeScriptPath } restore ${ absoluteProjectDir } ` ;
// Log a warning.
2019-08-03 10:37:19 -04:00
this . _warn ( [
` The project at " ${ absoluteProjectDir } " is running against the local Angular build. ` ,
'' ,
'To restore the npm packages run:' ,
'' ,
` " ${ restoreCmd } " ` ,
] . join ( '\n' ) ) ;
}
/ * *
* Log a warning message do draw user ' s attention .
* @ param { ... string [ ] } messages - The messages to be logged .
* /
_warn ( ... messages ) {
const lines = messages . join ( ' ' ) . split ( '\n' ) ;
2017-10-06 05:48:18 -04:00
console . warn ( chalk . yellow ( [
'' ,
'!' . repeat ( 110 ) ,
'!!!' ,
'!!! WARNING' ,
'!!!' ,
2019-08-03 10:37:19 -04:00
... lines . map ( line => ` !!! ${ line } ` ) ,
2017-10-06 05:48:18 -04:00
'!!!' ,
'!' . repeat ( 110 ) ,
'' ,
] . join ( '\n' ) ) ) ;
}
// Local marker helpers
_checkLocalMarker ( ) {
this . _log ( 'Checking for local marker at' , this . localMarkerPath ) ;
return fs . existsSync ( this . localMarkerPath ) ;
}
_setLocalMarker ( contents ) {
this . _log ( 'Writing local marker file to' , this . localMarkerPath ) ;
fs . writeFileSync ( this . localMarkerPath , contents ) ;
}
}
function main ( ) {
shelljs . set ( '-e' ) ;
2019-08-03 09:57:24 -04:00
const createInstaller = argv => {
const { projectDir , ... options } = argv ;
return new NgPackagesInstaller ( projectDir , options ) ;
} ;
2017-10-06 05:48:18 -04:00
yargs
. usage ( '$0 <cmd> [args]' )
. option ( 'debug' , { describe : 'Print additional debug information.' , default : false } )
. option ( 'force' , { describe : 'Force the command to execute even if not needed.' , default : false } )
2019-08-03 10:37:19 -04:00
. option ( 'build-packages' , { describe : 'Build the local Angular packages, before using them.' , default : false } )
2017-10-06 05:48:32 -04:00
. option ( 'ignore-packages' , { describe : 'List of Angular packages that should not be used in local mode.' , default : [ ] , array : true } )
2017-10-06 05:48:18 -04:00
2017-10-06 05:48:32 -04:00
. command ( 'overwrite <projectDir> [--force] [--debug] [--ignore-packages package1 package2]' , 'Install dependencies from the locally built Angular distributables.' , ( ) => { } , argv => {
2019-08-03 09:57:24 -04:00
createInstaller ( argv ) . installLocalDependencies ( ) ;
2017-10-06 05:48:18 -04:00
} )
. command ( 'restore <projectDir> [--debug]' , 'Install dependencies from the npm registry.' , ( ) => { } , argv => {
2019-08-03 09:57:24 -04:00
createInstaller ( argv ) . restoreNpmDependencies ( ) ;
2017-10-06 05:48:18 -04:00
} )
. command ( 'check <projectDir> [--debug]' , 'Check that dependencies came from npm. Otherwise display a warning message.' , ( ) => { } , argv => {
2019-08-03 09:57:24 -04:00
createInstaller ( argv ) . checkDependencies ( ) ;
2017-10-06 05:48:18 -04:00
} )
. demandCommand ( 1 , 'Please supply a command from the list above.' )
. strict ( )
. wrap ( yargs . terminalWidth ( ) )
. argv ;
}
module . exports = NgPackagesInstaller ;
if ( require . main === module ) {
main ( ) ;
2017-10-06 17:25:27 -04:00
}