2016-08-25 16:41:51 +08:00
'use strict' ;
const BbPromise = require ( 'bluebird' ) ;
2017-07-28 21:40:39 +02:00
const _ = require ( 'lodash' ) ;
2016-08-25 16:41:51 +08:00
const path = require ( 'path' ) ;
2017-07-28 21:40:39 +02:00
const fse = require ( 'fs-extra' ) ;
2017-08-09 18:12:59 +02:00
const isBuiltinModule = require ( 'is-builtin-module' ) ;
2016-08-25 16:41:51 +08:00
2018-02-28 18:49:19 +01:00
const Packagers = require ( './packagers' ) ;
2017-12-18 13:52:03 -02:00
function rebaseFileReferences ( pathToPackageRoot , moduleVersion ) {
2018-04-27 13:51:33 +02:00
if ( /^(?:file:[^/]{2}|\.\/|\.\.\/)/ . test ( moduleVersion ) ) {
2017-12-18 13:52:03 -02:00
const filePath = _ . replace ( moduleVersion , /^file:/ , '' ) ;
2018-04-27 13:51:33 +02:00
return _ . replace ( ` ${ _ . startsWith ( moduleVersion , 'file:' ) ? 'file:' : '' } ${ pathToPackageRoot } / ${ filePath } ` , /\\/g , '/' ) ;
2017-12-18 13:52:03 -02:00
}
return moduleVersion ;
}
2017-10-19 11:03:52 +02:00
/**
* Add the given modules to a package json's dependencies.
*/
2017-11-17 11:47:58 +01:00
function addModulesToPackageJson ( externalModules , packageJson , pathToPackageRoot ) {
2017-10-19 11:03:52 +02:00
_ . forEach ( externalModules , externalModule => {
const splitModule = _ . split ( externalModule , '@' ) ;
// If we have a scoped module we have to re-add the @
if ( _ . startsWith ( externalModule , '@' ) ) {
splitModule . splice ( 0 , 1 ) ;
splitModule [ 0 ] = '@' + splitModule [ 0 ] ;
}
2017-11-17 11:47:58 +01:00
let moduleVersion = _ . join ( _ . tail ( splitModule ) , '@' ) ;
// We have to rebase file references to the target package.json
2017-12-18 13:52:03 -02:00
moduleVersion = rebaseFileReferences ( pathToPackageRoot , moduleVersion ) ;
2017-10-19 11:03:52 +02:00
packageJson . dependencies = packageJson . dependencies || { } ;
packageJson . dependencies [ _ . first ( splitModule ) ] = moduleVersion ;
} ) ;
}
/**
* Remove a given list of excluded modules from a module list
* @this - The active plugin instance
*/
function removeExcludedModules ( modules , packageForceExcludes , log ) {
2018-03-21 10:45:04 +01:00
const excludedModules = _ . remove ( modules , externalModule => { // eslint-disable-line lodash/prefer-immutable-method
2017-10-19 11:03:52 +02:00
const splitModule = _ . split ( externalModule , '@' ) ;
// If we have a scoped module we have to re-add the @
if ( _ . startsWith ( externalModule , '@' ) ) {
splitModule . splice ( 0 , 1 ) ;
splitModule [ 0 ] = '@' + splitModule [ 0 ] ;
}
const moduleName = _ . first ( splitModule ) ;
return _ . includes ( packageForceExcludes , moduleName ) ;
} ) ;
if ( log && ! _ . isEmpty ( excludedModules ) ) {
this . serverless . cli . log ( ` Excluding external modules: ${ _ . join ( excludedModules , ', ' ) } ` ) ;
}
}
/**
2018-05-02 12:37:04 +02:00
* Resolve the needed versions of production dependencies for external modules.
2017-10-19 11:03:52 +02:00
* @this - The active plugin instance
*/
2018-05-02 12:37:04 +02:00
function getProdModules ( externalModules , packagePath , dependencyGraph , forceExcludes ) {
2017-08-09 18:12:59 +02:00
const packageJsonPath = path . join ( process . cwd ( ) , packagePath ) ;
const packageJson = require ( packageJsonPath ) ;
2016-09-07 11:55:02 +08:00
const prodModules = [ ] ;
// only process the module stated in dependencies section
if ( ! packageJson . dependencies ) {
2017-07-28 21:40:39 +02:00
return [ ] ;
2016-09-07 11:55:02 +08:00
}
2017-08-09 18:12:59 +02:00
// Get versions of all transient modules
2017-07-28 21:40:39 +02:00
_ . forEach ( externalModules , module => {
2017-08-09 18:12:59 +02:00
let moduleVersion = packageJson . dependencies [ module . external ] ;
2016-09-07 11:55:02 +08:00
if ( moduleVersion ) {
2017-08-09 18:12:59 +02:00
prodModules . push ( ` ${ module . external } @ ${ moduleVersion } ` ) ;
2017-09-21 11:58:53 +02:00
// Check if the module has any peer dependencies and include them too
try {
const modulePackagePath = path . join (
path . dirname ( path . join ( process . cwd ( ) , packagePath ) ) ,
'node_modules' ,
module . external ,
'package.json'
) ;
const peerDependencies = require ( modulePackagePath ) . peerDependencies ;
if ( ! _ . isEmpty ( peerDependencies ) ) {
this . options . verbose && this . serverless . cli . log ( ` Adding explicit peers for dependency ${ module . external } ` ) ;
2018-05-02 12:37:04 +02:00
const peerModules = getProdModules . call ( this , _ . map ( peerDependencies , ( value , key ) => ( { external : key } ) ) , packagePath , dependencyGraph , forceExcludes ) ;
2017-09-21 11:58:53 +02:00
Array . prototype . push . apply ( prodModules , peerModules ) ;
}
} catch ( e ) {
this . serverless . cli . log ( ` WARNING: Could not check for peer dependencies of ${ module . external } ` ) ;
}
2018-05-02 12:37:04 +02:00
} else {
if ( ! packageJson . devDependencies || ! packageJson . devDependencies [ module . external ] ) {
// Add transient dependencies if they appear not in the service's dev dependencies
const originInfo = _ . get ( dependencyGraph , 'dependencies' , { } ) [ module . origin ] || { } ;
moduleVersion = _ . get ( _ . get ( originInfo , 'dependencies' , { } ) [ module . external ] , 'version' ) ;
if ( ! moduleVersion ) {
this . serverless . cli . log ( ` WARNING: Could not determine version of module ${ module . external } ` ) ;
}
prodModules . push ( moduleVersion ? ` ${ module . external } @ ${ moduleVersion } ` : module . external ) ;
} else if ( packageJson . devDependencies && packageJson . devDependencies [ module . external ] && ! _ . includes ( forceExcludes , module . external ) ) {
2018-05-03 12:59:38 +02:00
// To minimize the chance of breaking setups we whitelist packages available on AWS here. These are due to the previously missing check
// most likely set in devDependencies and should not lead to an error now.
const ignoredDevDependencies = [ 'aws-sdk' ] ;
if ( ! _ . includes ( ignoredDevDependencies , module . external ) ) {
// Runtime dependency found in devDependencies but not forcefully excluded
this . serverless . cli . log ( ` ERROR: Runtime dependency ' ${ module . external } ' found in devDependencies. Move it to dependencies or use forceExclude to explicitly exclude it. ` ) ;
throw new this . serverless . classes . Error ( ` Serverless-webpack dependency error: ${ module . external } . ` ) ;
}
2018-05-16 10:09:22 +02:00
this . options . verbose && this . serverless . cli . log ( ` INFO: Runtime dependency ' ${ module . external } ' found in devDependencies. It has been excluded automatically. ` ) ;
2017-08-09 18:12:59 +02:00
}
2016-09-07 11:55:02 +08:00
}
} ) ;
return prodModules ;
}
function getExternalModuleName ( module ) {
const path = /^external "(.*)"$/ . exec ( module . identifier ( ) ) [ 1 ] ;
const pathComponents = path . split ( '/' ) ;
const main = pathComponents [ 0 ] ;
// this is a package within a namespace
if ( main . charAt ( 0 ) == '@' ) {
2017-07-28 21:40:39 +02:00
return ` ${ main } / ${ pathComponents [ 1 ] } ` ;
2016-09-07 11:55:02 +08:00
}
2017-07-28 21:40:39 +02:00
return main ;
2016-09-07 11:55:02 +08:00
}
function isExternalModule ( module ) {
2017-08-09 18:12:59 +02:00
return _ . startsWith ( module . identifier ( ) , 'external ' ) && ! isBuiltinModule ( getExternalModuleName ( module ) ) ;
}
/**
* Find the original module that required the transient dependency. Returns
* undefined if the module is a first level dependency.
* @param {Object} issuer - Module issuer
*/
function findExternalOrigin ( issuer ) {
if ( ! _ . isNil ( issuer ) && _ . startsWith ( issuer . rawRequest , './' ) ) {
return findExternalOrigin ( issuer . issuer ) ;
}
return issuer ;
2016-09-07 11:55:02 +08:00
}
function getExternalModules ( stats ) {
const externals = new Set ( ) ;
2017-07-28 21:40:39 +02:00
_ . forEach ( stats . compilation . chunks , chunk => {
2016-09-07 11:55:02 +08:00
// Explore each module within the chunk (built inputs):
2018-02-26 04:50:51 -05:00
chunk . forEachModule ( module => {
2016-09-07 11:55:02 +08:00
if ( isExternalModule ( module ) ) {
2017-08-09 18:12:59 +02:00
externals . add ( {
origin : _ . get ( findExternalOrigin ( module . issuer ) , 'rawRequest' ) ,
external : getExternalModuleName ( module )
} ) ;
2016-09-07 11:55:02 +08:00
}
} ) ;
} ) ;
return Array . from ( externals ) ;
}
2016-08-25 16:41:51 +08:00
module . exports = {
2017-07-28 21:40:39 +02:00
/**
* We need a performant algorithm to install the packages for each single
* function (in case we package individually).
* (1) We fetch ALL packages needed by ALL functions in a first step
* and use this as a base npm checkout. The checkout will be done to a
* separate temporary directory with a package.json that contains everything.
* (2) For each single compile we copy the whole node_modules to the compile
* directory and create a (function) compile specific package.json and store
* it in the compile directory. Now we start npm again there, and npm will just
* remove the superfluous packages and optimize the remaining dependencies.
* This will utilize the npm cache at its best and give us the needed results
* and performance.
*/
2017-10-23 07:29:17 -06:00
packExternalModules ( ) {
const stats = this . compileStats ;
2016-08-25 16:41:51 +08:00
2018-03-07 19:17:38 +01:00
const includes = this . configuration . includeModules ;
2016-08-25 16:41:51 +08:00
2017-07-28 21:40:39 +02:00
if ( ! includes ) {
2017-10-23 07:29:17 -06:00
return BbPromise . resolve ( ) ;
2017-07-28 21:40:39 +02:00
}
2016-08-25 16:41:51 +08:00
2017-10-19 11:03:52 +02:00
// Read plugin configuration
2017-09-19 12:47:56 +02:00
const packageForceIncludes = _ . get ( includes , 'forceInclude' , [ ] ) ;
2017-10-19 11:03:52 +02:00
const packageForceExcludes = _ . get ( includes , 'forceExclude' , [ ] ) ;
2017-07-28 21:40:39 +02:00
const packagePath = includes . packagePath || './package.json' ;
2017-08-09 18:12:59 +02:00
const packageJsonPath = path . join ( process . cwd ( ) , packagePath ) ;
2018-03-09 17:48:20 +01:00
const packageScripts = _ . reduce ( this . configuration . packagerOptions . scripts || [ ] , ( _ _ , script , index ) => {
_ _ [ ` script ${ index } ` ] = script ;
return _ _ ;
} , { } ) ;
2017-08-09 18:12:59 +02:00
2018-02-28 18:49:19 +01:00
// Determine and create packager
2018-03-07 01:20:15 +01:00
return BbPromise . try ( ( ) => Packagers . get . call ( this , this . configuration . packager ) )
2018-02-28 18:49:19 +01:00
. then ( packager => {
2018-04-30 11:11:06 +02:00
// Fetch needed original package.json sections
const sectionNames = packager . copyPackageSectionNames ;
const packageJson = this . serverless . utils . readFileSync ( packageJsonPath ) ;
const packageSections = _ . pick ( packageJson , sectionNames ) ;
if ( ! _ . isEmpty ( packageSections ) ) {
this . options . verbose && this . serverless . cli . log ( ` Using package.json sections ${ _ . join ( _ . keys ( packageSections ) , ', ' ) } ` ) ;
}
2018-02-28 18:49:19 +01:00
// Get first level dependency graph
this . options . verbose && this . serverless . cli . log ( ` Fetch dependency graph from ${ packageJsonPath } ` ) ;
2017-09-07 13:42:50 +02:00
2018-04-25 12:25:59 +02:00
return packager . getProdDependencies ( path . dirname ( packageJsonPath ) , 1 )
2018-02-28 18:49:19 +01:00
. then ( dependencyGraph => {
const problems = _ . get ( dependencyGraph , 'problems' , [ ] ) ;
if ( this . options . verbose && ! _ . isEmpty ( problems ) ) {
this . serverless . cli . log ( ` Ignoring ${ _ . size ( problems ) } NPM errors: ` ) ;
_ . forEach ( problems , problem => {
this . serverless . cli . log ( ` => ${ problem } ` ) ;
} ) ;
2017-08-05 01:20:38 +02:00
}
2018-02-28 18:49:19 +01:00
// (1) Generate dependency composition
const compositeModules = _ . uniq ( _ . flatMap ( stats . stats , compileStats => {
const externalModules = _ . concat (
2017-09-19 12:47:56 +02:00
getExternalModules . call ( this , compileStats ) ,
_ . map ( packageForceIncludes , whitelistedPackage => ( { external : whitelistedPackage } ) )
2018-02-28 18:49:19 +01:00
) ;
2018-05-02 12:37:04 +02:00
return getProdModules . call ( this , externalModules , packagePath , dependencyGraph , packageForceExcludes ) ;
2018-02-28 18:49:19 +01:00
} ) ) ;
removeExcludedModules . call ( this , compositeModules , packageForceExcludes , true ) ;
if ( _ . isEmpty ( compositeModules ) ) {
// The compiled code does not reference any external modules at all
this . serverless . cli . log ( 'No external modules needed' ) ;
2017-10-31 14:58:54 +01:00
return BbPromise . resolve ( ) ;
2017-10-30 11:17:05 +11:00
}
2018-02-28 18:49:19 +01:00
// (1.a) Install all needed modules
const compositeModulePath = path . join ( this . webpackOutputPath , 'dependencies' ) ;
const compositePackageJson = path . join ( compositeModulePath , 'package.json' ) ;
// (1.a.1) Create a package.json
2018-04-30 11:11:06 +02:00
const compositePackage = _ . defaults ( {
2018-02-28 18:49:19 +01:00
name : this . serverless . service . service ,
version : '1.0.0' ,
description : ` Packaged externals for ${ this . serverless . service . service } ` ,
2018-03-09 17:48:20 +01:00
private : true ,
scripts : packageScripts
2018-04-30 11:11:06 +02:00
} , packageSections ) ;
2018-02-28 18:49:19 +01:00
const relPath = path . relative ( compositeModulePath , path . dirname ( packageJsonPath ) ) ;
addModulesToPackageJson ( compositeModules , compositePackage , relPath ) ;
this . serverless . utils . writeFileSync ( compositePackageJson , JSON . stringify ( compositePackage , null , 2 ) ) ;
// (1.a.2) Copy package-lock.json if it exists, to prevent unwanted upgrades
const packageLockPath = path . join ( path . dirname ( packageJsonPath ) , packager . lockfileName ) ;
2018-03-08 02:57:03 +01:00
let hasPackageLock = false ;
2018-02-28 18:49:19 +01:00
return BbPromise . fromCallback ( cb => fse . pathExists ( packageLockPath , cb ) )
. then ( exists => {
if ( exists ) {
this . serverless . cli . log ( 'Package lock found - Using locked versions' ) ;
try {
2018-04-27 13:44:49 +02:00
let packageLockFile = this . serverless . utils . readFileSync ( packageLockPath ) ;
packageLockFile = packager . rebaseLockfile ( relPath , packageLockFile ) ;
if ( _ . isObject ( packageLockFile ) ) {
packageLockFile = JSON . stringify ( packageLockFile , null , 2 ) ;
2018-03-08 02:57:03 +01:00
}
2018-02-28 18:49:19 +01:00
2018-04-27 13:44:49 +02:00
this . serverless . utils . writeFileSync ( path . join ( compositeModulePath , packager . lockfileName ) , packageLockFile ) ;
2018-03-08 02:57:03 +01:00
hasPackageLock = true ;
2018-02-28 18:49:19 +01:00
} catch ( err ) {
this . serverless . cli . log ( ` Warning: Could not read lock file: ${ err . message } ` ) ;
}
}
return BbPromise . resolve ( ) ;
} )
2017-10-31 14:58:54 +01:00
. then ( ( ) => {
2018-02-28 18:49:19 +01:00
const start = _ . now ( ) ;
this . serverless . cli . log ( 'Packing external modules: ' + compositeModules . join ( ', ' ) ) ;
2018-04-25 12:25:59 +02:00
return packager . install ( compositeModulePath , this . configuration . packagerOptions )
2018-02-28 18:49:19 +01:00
. then ( ( ) => this . options . verbose && this . serverless . cli . log ( ` Package took [ ${ _ . now ( ) - start } ms] ` ) )
. return ( stats . stats ) ;
} )
. mapSeries ( compileStats => {
const modulePath = compileStats . compilation . compiler . outputPath ;
// Create package.json
const modulePackageJson = path . join ( modulePath , 'package.json' ) ;
2018-04-30 11:11:06 +02:00
const modulePackage = _ . defaults ( {
2018-03-09 17:48:20 +01:00
name : this . serverless . service . service ,
version : '1.0.0' ,
description : ` Packaged externals for ${ this . serverless . service . service } ` ,
private : true ,
scripts : packageScripts ,
2018-02-28 18:49:19 +01:00
dependencies : { }
2018-04-30 11:11:06 +02:00
} , packageSections ) ;
2018-02-28 18:49:19 +01:00
const prodModules = getProdModules . call ( this ,
_ . concat (
getExternalModules . call ( this , compileStats ) ,
_ . map ( packageForceIncludes , whitelistedPackage => ( { external : whitelistedPackage } ) )
2018-05-02 12:37:04 +02:00
) , packagePath , dependencyGraph , packageForceExcludes ) ;
2018-02-28 18:49:19 +01:00
removeExcludedModules . call ( this , prodModules , packageForceExcludes ) ;
const relPath = path . relative ( modulePath , path . dirname ( packageJsonPath ) ) ;
addModulesToPackageJson ( prodModules , modulePackage , relPath ) ;
this . serverless . utils . writeFileSync ( modulePackageJson , JSON . stringify ( modulePackage , null , 2 ) ) ;
// GOOGLE: Copy modules only if not google-cloud-functions
// GCF Auto installs the package json
if ( _ . get ( this . serverless , 'service.provider.name' ) === 'google' ) {
return BbPromise . resolve ( ) ;
}
const startCopy = _ . now ( ) ;
2018-03-08 02:57:03 +01:00
return BbPromise . try ( ( ) => {
// Only copy dependency modules if demanded by packager
if ( packager . mustCopyModules ) {
return BbPromise . fromCallback ( callback => fse . copy ( path . join ( compositeModulePath , 'node_modules' ) , path . join ( modulePath , 'node_modules' ) , callback ) ) ;
}
return BbPromise . resolve ( ) ;
} )
. then ( ( ) => hasPackageLock ?
BbPromise . fromCallback ( callback => fse . copy ( path . join ( compositeModulePath , packager . lockfileName ) , path . join ( modulePath , packager . lockfileName ) , callback ) ) :
BbPromise . resolve ( )
)
2018-02-28 18:49:19 +01:00
. tap ( ( ) => this . options . verbose && this . serverless . cli . log ( ` Copy modules: ${ modulePath } [ ${ _ . now ( ) - startCopy } ms] ` ) )
. then ( ( ) => {
// Prune extraneous packages - removes not needed ones
const startPrune = _ . now ( ) ;
2018-04-25 12:25:59 +02:00
return packager . prune ( modulePath , this . configuration . packagerOptions )
2018-02-28 18:49:19 +01:00
. tap ( ( ) => this . options . verbose && this . serverless . cli . log ( ` Prune: ${ modulePath } [ ${ _ . now ( ) - startPrune } ms] ` ) ) ;
2018-03-09 17:48:20 +01:00
} )
. then ( ( ) => {
// Prune extraneous packages - removes not needed ones
const startRunScripts = _ . now ( ) ;
2018-04-25 12:25:59 +02:00
return packager . runScripts ( modulePath , _ . keys ( packageScripts ) )
2018-03-09 17:48:20 +01:00
. tap ( ( ) => this . options . verbose && this . serverless . cli . log ( ` Run scripts: ${ modulePath } [ ${ _ . now ( ) - startRunScripts } ms] ` ) ) ;
2018-02-28 18:49:19 +01:00
} ) ;
} )
. return ( ) ;
} ) ;
2017-09-07 13:42:50 +02:00
} ) ;
2016-09-07 11:55:02 +08:00
}
2016-08-25 16:41:51 +08:00
} ;