diff --git a/README.md b/README.md index 8d08df3ad6..6426059f3a 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,22 @@ custom: ``` > Note that only relative path is supported at the moment. + +`peerDependencies` of all above external dependencies will also be packed into the Serverless +artifact. By default, `node_modules` in the same directory as `package.json` (current working directory +or specified by`packagePath`) will be used. + +However in some configuration (like monorepo), `node_modules` is in parent directory which is different from +where `package.json` is. Set `nodeModulesRelativeDir` to specify the relative directory where `node_modules` is. + +```yaml +# serverless.yml +custom: + webpack: + includeModules: + nodeModulesRelativeDir: '../../' # relative path to current working directory. +``` + #### Runtime dependencies If a runtime dependency is detected that is found in the `devDependencies` section and diff --git a/lib/packExternalModules.js b/lib/packExternalModules.js index 96b4a4c43e..e1cf43888e 100644 --- a/lib/packExternalModules.js +++ b/lib/packExternalModules.js @@ -66,7 +66,7 @@ function removeExcludedModules(modules, packageForceExcludes, log) { * Resolve the needed versions of production dependencies for external modules. * @this - The active plugin instance */ -function getProdModules(externalModules, packagePath, dependencyGraph, forceExcludes) { +function getProdModules(externalModules, packagePath, nodeModulesRelativeDir, dependencyGraph, forceExcludes) { const packageJsonPath = path.join(process.cwd(), packagePath); const packageJson = require(packageJsonPath); const prodModules = []; @@ -83,14 +83,24 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl if (moduleVersion) { prodModules.push(`${module.external}@${moduleVersion}`); + let nodeModulesBase = path.join(path.dirname(path.join(process.cwd(), packagePath)), 'node_modules'); + + if (nodeModulesRelativeDir) { + const customNodeModulesDir = path.join(process.cwd(), nodeModulesRelativeDir, 'node_modules'); + + if (fse.pathExistsSync(customNodeModulesDir)) { + nodeModulesBase = customNodeModulesDir; + } else { + this.serverless.cli.log( + `WARNING: ${customNodeModulesDir} dose not exist. Please check nodeModulesRelativeDir setting` + ); + } + } + // 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 modulePackagePath = path.join(nodeModulesBase, 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}`); @@ -115,6 +125,7 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl this, _.map(peerDependencies, (value, key) => ({ external: key })), packagePath, + nodeModulesRelativeDir, dependencyGraph, forceExcludes ); @@ -122,7 +133,9 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl } } } catch (e) { - this.serverless.cli.log(`WARNING: Could not check for peer dependencies of ${module.external}`); + this.serverless.cli.log( + `WARNING: Could not check for peer dependencies of ${module.external}. Set nodeModulesRelativeDir if node_modules is in different directory.` + ); } } else { if (!packageJson.devDependencies || !packageJson.devDependencies[module.external]) { @@ -130,7 +143,7 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl const originInfo = _.get(dependencyGraph, 'dependencies', {})[module.origin] || {}; moduleVersion = _.get(_.get(originInfo, 'dependencies', {})[module.external], 'version'); if (!moduleVersion) { - // eslint-disable-next-line lodash/path-style + // eslint-disable-next-line lodash/path-style moduleVersion = _.get(dependencyGraph, [ 'dependencies', module.external, 'version' ]); } if (!moduleVersion) { @@ -248,6 +261,7 @@ module.exports = { const packageForceIncludes = _.get(includes, 'forceInclude', []); const packageForceExcludes = _.get(includes, 'forceExclude', []); const packagePath = includes.packagePath || './package.json'; + const nodeModulesRelativeDir = includes.nodeModulesRelativeDir; const packageJsonPath = path.join(process.cwd(), packagePath); const packageScripts = _.reduce( this.configuration.packagerOptions.scripts || [], @@ -290,7 +304,14 @@ module.exports = { external: whitelistedPackage })) ); - return getProdModules.call(this, externalModules, packagePath, dependencyGraph, packageForceExcludes); + return getProdModules.call( + this, + externalModules, + packagePath, + nodeModulesRelativeDir, + dependencyGraph, + packageForceExcludes + ); }) ); removeExcludedModules.call(this, compositeModules, packageForceExcludes, true); @@ -378,6 +399,7 @@ module.exports = { })) ), packagePath, + nodeModulesRelativeDir, dependencyGraph, packageForceExcludes ); diff --git a/tests/packExternalModules.test.js b/tests/packExternalModules.test.js index 69999943ab..54ec93c120 100644 --- a/tests/packExternalModules.test.js +++ b/tests/packExternalModules.test.js @@ -128,6 +128,7 @@ describe('packExternalModules', () => { writeFileSyncStub.restore(); readFileSyncStub.restore(); fsExtraMock.pathExists.reset(); + fsExtraMock.pathExistsSync.reset(); fsExtraMock.copy.reset(); sandbox.reset(); }); @@ -1244,109 +1245,166 @@ describe('packExternalModules', () => { }); describe('optional behavior', () => { - before(() => { - const peerDepPackageJson = require('./data/package-peerdeps.json'); - mockery.deregisterMock(path.join(process.cwd(), 'package.json')); - mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson); - // Mock request-promise package.json - const rpPackageJson = require('./data/rp-package-optional.json'); - const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json'); - mockery.registerMock(rpPackagePath, rpPackageJson); - }); - - after(() => { - mockery.deregisterMock(path.join(process.cwd(), 'package.json')); - mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock); - const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json'); - mockery.deregisterMock(rpPackagePath); - }); + const expectedCompositePackageJSON = { + name: 'test-service', + version: '1.0.0', + description: 'Packaged externals for test-service', + private: true, + scripts: {}, + dependencies: { + bluebird: '^3.5.0', + 'request-promise': '^4.2.1', + request: '^2.82.0' + } + }; + const expectedPackageJSON = { + name: 'test-service', + version: '1.0.0', + description: 'Packaged externals for test-service', + private: true, + scripts: {}, + dependencies: { + bluebird: '^3.5.0', + 'request-promise': '^4.2.1', + request: '^2.82.0' + } + }; - it('should skip optional peer dependencies', () => { - const expectedCompositePackageJSON = { - name: 'test-service', - version: '1.0.0', - description: 'Packaged externals for test-service', - private: true, - scripts: {}, - dependencies: { - bluebird: '^3.5.0', - 'request-promise': '^4.2.1', - request: '^2.82.0' - } - }; - const expectedPackageJSON = { - name: 'test-service', - version: '1.0.0', - description: 'Packaged externals for test-service', - private: true, - scripts: {}, - dependencies: { - bluebird: '^3.5.0', - 'request-promise': '^4.2.1', - request: '^2.82.0' + const dependencyGraph = require('./data/npm-ls-peerdeps.json'); + const peerDepStats = { + stats: [ + { + compilation: { + chunks: [ + new ChunkMock([ + { + identifier: _.constant('"crypto"') + }, + { + identifier: _.constant('"uuid/v4"') + }, + { + identifier: _.constant('"mockery"') + }, + { + identifier: _.constant('"@scoped/vendor/module1"') + }, + { + identifier: _.constant('external "bluebird"') + }, + { + identifier: _.constant('external "request-promise"') + } + ]) + ], + compiler: { + outputPath: '/my/Service/Path/.webpack/service' + } + } } - }; + ] + }; - const dependencyGraph = require('./data/npm-ls-peerdeps.json'); - const peerDepStats = { - stats: [ - { - compilation: { - chunks: [ - new ChunkMock([ - { - identifier: _.constant('"crypto"') - }, - { - identifier: _.constant('"uuid/v4"') - }, - { - identifier: _.constant('"mockery"') - }, - { - identifier: _.constant('"@scoped/vendor/module1"') - }, - { - identifier: _.constant('external "bluebird"') - }, - { - identifier: _.constant('external "request-promise"') - } - ]) - ], - compiler: { - outputPath: '/my/Service/Path/.webpack/service' - } + describe('without nodeModulesRelativeDir', () => { + before(() => { + const peerDepPackageJson = require('./data/package-peerdeps.json'); + mockery.deregisterMock(path.join(process.cwd(), 'package.json')); + mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson); + // Mock request-promise package.json + const rpPackageJson = require('./data/rp-package-optional.json'); + const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json'); + mockery.registerMock(rpPackagePath, rpPackageJson); + }); + + after(() => { + mockery.deregisterMock(path.join(process.cwd(), 'package.json')); + mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock); + const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json'); + mockery.deregisterMock(rpPackagePath); + }); + + it('should skip optional peer dependencies', () => { + module.webpackOutputPath = 'outputPath'; + fsExtraMock.pathExists.yields(null, false); + fsExtraMock.copy.yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph)); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); + packagerMock.runScripts.returns(BbPromise.resolve()); + module.compileStats = peerDepStats; + return expect(module.packExternalModules()).to.be.fulfilled.then(() => + BbPromise.all([ + // The module package JSON and the composite one should have been stored + expect(writeFileSyncStub).to.have.been.calledTwice, + expect(writeFileSyncStub.firstCall.args[1]).to.equal( + JSON.stringify(expectedCompositePackageJSON, null, 2) + ), + expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)), + // The modules should have been copied + expect(fsExtraMock.copy).to.have.been.calledOnce, + // npm ls and npm prune should have been called + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, + expect(packagerMock.runScripts).to.have.been.calledOnce + ]) + ); + }); + }); + + describe('with nodeModulesRelativeDir', () => { + before(() => { + const peerDepPackageJson = require('./data/package-peerdeps.json'); + mockery.deregisterMock(path.join(process.cwd(), 'package.json')); + mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson); + // Mock request-promise package.json + const rpPackageJson = require('./data/rp-package-optional.json'); + const rpPackagePath = path.join(process.cwd(), '../../', 'node_modules', 'request-promise', 'package.json'); + mockery.registerMock(rpPackagePath, rpPackageJson); + }); + + after(() => { + mockery.deregisterMock(path.join(process.cwd(), 'package.json')); + mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock); + const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json'); + mockery.deregisterMock(rpPackagePath); + }); + + it('should skip optional peer dependencies', () => { + module.configuration = new Configuration({ + webpack: { + includeModules: { + nodeModulesRelativeDir: '../../' } } - ] - }; - - module.webpackOutputPath = 'outputPath'; - fsExtraMock.pathExists.yields(null, false); - fsExtraMock.copy.yields(); - packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph)); - packagerMock.install.returns(BbPromise.resolve()); - packagerMock.prune.returns(BbPromise.resolve()); - packagerMock.runScripts.returns(BbPromise.resolve()); - module.compileStats = peerDepStats; - return expect(module.packExternalModules()).to.be.fulfilled.then(() => - BbPromise.all([ - // The module package JSON and the composite one should have been stored - expect(writeFileSyncStub).to.have.been.calledTwice, - expect(writeFileSyncStub.firstCall.args[1]).to.equal( - JSON.stringify(expectedCompositePackageJSON, null, 2) - ), - expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)), - // The modules should have been copied - expect(fsExtraMock.copy).to.have.been.calledOnce, - // npm ls and npm prune should have been called - expect(packagerMock.getProdDependencies).to.have.been.calledOnce, - expect(packagerMock.install).to.have.been.calledOnce, - expect(packagerMock.prune).to.have.been.calledOnce, - expect(packagerMock.runScripts).to.have.been.calledOnce - ]) - ); + }); + module.webpackOutputPath = 'outputPath'; + fsExtraMock.pathExists.yields(null, false); + fsExtraMock.pathExistsSync.returns(true); + fsExtraMock.copy.yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph)); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); + packagerMock.runScripts.returns(BbPromise.resolve()); + module.compileStats = peerDepStats; + return expect(module.packExternalModules()).to.be.fulfilled.then(() => + BbPromise.all([ + // The module package JSON and the composite one should have been stored + expect(writeFileSyncStub).to.have.been.calledTwice, + expect(writeFileSyncStub.firstCall.args[1]).to.equal( + JSON.stringify(expectedCompositePackageJSON, null, 2) + ), + expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)), + // The modules should have been copied + expect(fsExtraMock.copy).to.have.been.calledOnce, + // npm ls and npm prune should have been called + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, + expect(packagerMock.runScripts).to.have.been.calledOnce + ]) + ); + }); }); }); });