diff --git a/.npmignore b/.npmignore index 1ca47212f..541f88d68 100644 --- a/.npmignore +++ b/.npmignore @@ -2,3 +2,4 @@ node_modules coverage examples tests +*.test.js diff --git a/lib/packExternalModules.js b/lib/packExternalModules.js index c9b33564e..ee71b8974 100644 --- a/lib/packExternalModules.js +++ b/lib/packExternalModules.js @@ -3,10 +3,11 @@ const BbPromise = require('bluebird'); const _ = require('lodash'); const path = require('path'); -const childProcess = require('child_process'); const fse = require('fs-extra'); const isBuiltinModule = require('is-builtin-module'); +const Packagers = require('./packagers'); + function rebaseFileReferences(pathToPackageRoot, moduleVersion) { if (/^file:[^/]{2}/.test(moduleVersion)) { const filePath = _.replace(moduleVersion, /^file:/, ''); @@ -16,18 +17,6 @@ function rebaseFileReferences(pathToPackageRoot, moduleVersion) { return moduleVersion; } -function rebasePackageLock(pathToPackageRoot, module) { - if (module.version) { - module.version = rebaseFileReferences(pathToPackageRoot, module.version); - } - - if (module.dependencies) { - _.forIn(module.dependencies, moduleDependency => { - rebasePackageLock(pathToPackageRoot, moduleDependency); - }); - } -} - /** * Add the given modules to a package json's dependencies. */ @@ -200,156 +189,121 @@ module.exports = { const packagePath = includes.packagePath || './package.json'; const packageJsonPath = path.join(process.cwd(), packagePath); - this.options.verbose && this.serverless.cli.log(`Fetch dependency graph from ${packageJsonPath}`); - // Get first level dependency graph - const command = 'npm ls -prod -json -depth=1'; // Only prod dependencies - - const ignoredNpmErrors = [ - { npmError: 'extraneous', log: false }, - { npmError: 'missing', log: false }, - { npmError: 'peer dep missing', log: true }, - ]; - - return BbPromise.fromCallback(cb => { - childProcess.exec(command, { - cwd: path.dirname(packageJsonPath), - maxBuffer: this.serverless.service.custom.packExternalModulesMaxBuffer || 200 * 1024, - encoding: 'utf8' - }, (err, stdout, stderr) => { - if (err) { - // Only exit with an error if we have critical npm errors for 2nd level inside - const errors = _.split(stderr, '\n'); - const failed = _.reduce(errors, (failed, error) => { - if (failed) { - return true; - } - return !_.isEmpty(error) && !_.some(ignoredNpmErrors, ignoredError => _.startsWith(error, `npm ERR! ${ignoredError.npmError}`)); - }, false); - - if (failed) { - return cb(err); - } - } - return cb(null, stdout); - }); - }) - .then(depJson => BbPromise.try(() => JSON.parse(depJson))) - .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}`); - }); - } - - // (1) Generate dependency composition - const compositeModules = _.uniq(_.flatMap(stats.stats, compileStats => { - const externalModules = _.concat( - getExternalModules.call(this, compileStats), - _.map(packageForceIncludes, whitelistedPackage => ({ external: whitelistedPackage })) - ); - return getProdModules.call(this, externalModules, packagePath, dependencyGraph); - })); - 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'); - return BbPromise.resolve(); - } - - // (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 - const compositePackage = { - name: this.serverless.service.service, - version: '1.0.0', - description: `Packaged externals for ${this.serverless.service.service}`, - private: true - }; - 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), 'package-lock.json'); - return BbPromise.fromCallback(cb => fse.pathExists(packageLockPath, cb)) - .then(exists => { - if (exists) { - this.serverless.cli.log('Package lock found - Using locked versions'); - try { - const packageLockJson = this.serverless.utils.readFileSync(packageLockPath); - /** - * We should not be modifying 'package-lock.json' - * because this file should be treat as internal to npm. - * - * Rebase package-lock is a temporary workaround and must be - * removed as soon as https://github.com/npm/npm/issues/19183 gets fixed. - */ - rebasePackageLock(relPath, packageLockJson); - - this.serverless.utils.writeFileSync(path.join(compositeModulePath, 'package-lock.json'), JSON.stringify(packageLockJson, null, 2)); - } catch(err) { - this.serverless.cli.log(`Warning: Could not read lock file: ${err.message}`); - } + // Determine and create packager + return BbPromise.try(() => Packagers.get.call(this, 'npm')) + .then(packager => { + // Get first level dependency graph + this.options.verbose && this.serverless.cli.log(`Fetch dependency graph from ${packageJsonPath}`); + const maxExecBufferSize = this.serverless.service.custom.packExternalModulesMaxBuffer || 200 * 1024; + + return packager.getProdDependencies(path.dirname(packageJsonPath), 1, maxExecBufferSize) + .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}`); + }); } - return BbPromise.resolve(); - }) - .then(() => { - const start = _.now(); - this.serverless.cli.log('Packing external modules: ' + compositeModules.join(', ')); - return BbPromise.fromCallback(cb => { - childProcess.exec('npm install', { - cwd: compositeModulePath, - maxBuffer: this.serverless.service.custom.packExternalModulesMaxBuffer || 200 * 1024, - encoding: 'utf8' - }, cb); - }) - .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'); - const modulePackage = { - dependencies: {} - }; - const prodModules = getProdModules.call(this, - _.concat( + + // (1) Generate dependency composition + const compositeModules = _.uniq(_.flatMap(stats.stats, compileStats => { + const externalModules = _.concat( getExternalModules.call(this, compileStats), _.map(packageForceIncludes, whitelistedPackage => ({ external: whitelistedPackage })) - ), packagePath, dependencyGraph); - 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 getProdModules.call(this, externalModules, packagePath, dependencyGraph); + })); + 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'); return BbPromise.resolve(); } - - const startCopy = _.now(); - return BbPromise.fromCallback(callback => fse.copy(path.join(compositeModulePath, 'node_modules'), path.join(modulePath, 'node_modules'), callback)) - .tap(() => this.options.verbose && this.serverless.cli.log(`Copy modules: ${modulePath} [${_.now() - startCopy} ms]`)) + + // (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 + const compositePackage = { + name: this.serverless.service.service, + version: '1.0.0', + description: `Packaged externals for ${this.serverless.service.service}`, + private: true + }; + 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); + return BbPromise.fromCallback(cb => fse.pathExists(packageLockPath, cb)) + .then(exists => { + if (exists) { + this.serverless.cli.log('Package lock found - Using locked versions'); + try { + const packageLockJson = this.serverless.utils.readFileSync(packageLockPath); + /** + * We should not be modifying 'package-lock.json' + * because this file should be treat as internal to npm. + * + * Rebase package-lock is a temporary workaround and must be + * removed as soon as https://github.com/npm/npm/issues/19183 gets fixed. + */ + packager.rebaseLockfile(relPath, packageLockJson); + + this.serverless.utils.writeFileSync(path.join(compositeModulePath, packager.lockfileName), JSON.stringify(packageLockJson, null, 2)); + } catch(err) { + this.serverless.cli.log(`Warning: Could not read lock file: ${err.message}`); + } + } + return BbPromise.resolve(); + }) .then(() => { - // Prune extraneous packages - removes not needed ones - const startPrune = _.now(); - return BbPromise.fromCallback(callback => { - childProcess.exec('npm prune', { - cwd: modulePath - }, callback); - }) - .tap(() => this.options.verbose && this.serverless.cli.log(`Prune: ${modulePath} [${_.now() - startPrune} ms]`)); - }); - }) - .return(); + const start = _.now(); + this.serverless.cli.log('Packing external modules: ' + compositeModules.join(', ')); + return packager.install(compositeModulePath, maxExecBufferSize) + .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'); + const modulePackage = { + dependencies: {} + }; + const prodModules = getProdModules.call(this, + _.concat( + getExternalModules.call(this, compileStats), + _.map(packageForceIncludes, whitelistedPackage => ({ external: whitelistedPackage })) + ), packagePath, dependencyGraph); + 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(); + return BbPromise.fromCallback(callback => fse.copy(path.join(compositeModulePath, 'node_modules'), path.join(modulePath, 'node_modules'), callback)) + .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(); + return packager.prune(modulePath, maxExecBufferSize) + .tap(() => this.options.verbose && this.serverless.cli.log(`Prune: ${modulePath} [${_.now() - startPrune} ms]`)); + }); + }) + .return(); + }); }); } }; diff --git a/lib/packagers/index.js b/lib/packagers/index.js new file mode 100644 index 000000000..790782f44 --- /dev/null +++ b/lib/packagers/index.js @@ -0,0 +1,38 @@ +'use strict'; +/** + * Factory for supported packagers. + * + * All packagers must implement the following interface: + * + * interface Packager { + * + * static get lockfileName(): string; + * static getProdDependencies(cwd: string, depth: number = 1, maxExecBufferSize = undefined): BbPromise; + * static rebaseLockfile(pathToPackageRoot: string, lockfile: Object): void; + * static install(cwd: string, maxExecBufferSize = undefined): BbPromise; + * static prune(cwd: string): BbPromise; + * + * } + */ + +const _ = require('lodash'); +const npm = require('./npm'); + +const registeredPackagers = { + npm: npm +}; + +/** + * Factory method. + * @this ServerlessWebpack - Active plugin instance + * @param {string} packagerId - Well known packager id. + * @returns {BbPromise} - Promised packager to allow packagers be created asynchronously. + */ +module.exports.get = function(packagerId) { + if (!_.has(registeredPackagers, packagerId)) { + const message = `Could not find packager '${packagerId}'`; + this.serverless.cli.log(`ERROR: ${message}`); + throw new this.serverless.classes.Error(message); + } + return registeredPackagers[packagerId]; +}; diff --git a/lib/packagers/index.test.js b/lib/packagers/index.test.js new file mode 100644 index 000000000..988b37b14 --- /dev/null +++ b/lib/packagers/index.test.js @@ -0,0 +1,68 @@ +'use strict'; +/** + * Unit tests for packagers/index + */ + +const _ = require('lodash'); +const chai = require('chai'); +const sinon = require('sinon'); +const mockery = require('mockery'); +const Serverless = require('serverless'); + +chai.use(require('sinon-chai')); + +const expect = chai.expect; + +describe('packagers factory', () => { + let sandbox; + let serverless; + let npmMock; + let baseModule; + let module; + + before(() => { + sandbox = sinon.sandbox.create(); + npmMock = { + hello: 'I am NPM' + }; + mockery.enable({ useCleanCache: true }); + mockery.registerAllowables([ './index', 'lodash' ]); + mockery.registerMock('./npm', npmMock); + baseModule = require('./index'); + Object.freeze(baseModule); + }); + + after(() => { + mockery.disable(); + mockery.deregisterAll(); + }); + + beforeEach(() => { + serverless = new Serverless(); + serverless.cli = { + log: sandbox.stub(), + consoleLog: sandbox.stub() + }; + + module = _.assign({ + serverless, + options: { + verbose: true + }, + }, baseModule); + }); + + afterEach(() => { + sandbox.reset(); + sandbox.restore(); + }); + + it('should throw on unknown packagers', () => { + expect(() => module.get.call({ serverless }, 'unknown')).to.throw(/Could not find packager/); + }); + + it('should return npm packager', () => { + const npm = module.get.call(module, 'npm'); + expect(npm).to.deep.equal(npmMock); + }); +}); diff --git a/lib/packagers/npm.js b/lib/packagers/npm.js new file mode 100644 index 000000000..da5a7caab --- /dev/null +++ b/lib/packagers/npm.js @@ -0,0 +1,95 @@ +'use strict'; +/** + * NPM packager. + */ + +const _ = require('lodash'); +const BbPromise = require('bluebird'); +const childProcess = require('child_process'); + +class NPM { + static get lockfileName() { // eslint-disable-line lodash/prefer-constant + return 'package-lock.json'; + } + + static getProdDependencies(cwd, depth, maxExecBufferSize) { + // Get first level dependency graph + const command = `npm ls -prod -json -depth=${depth || 1}`; // Only prod dependencies + + const ignoredNpmErrors = [ + { npmError: 'extraneous', log: false }, + { npmError: 'missing', log: false }, + { npmError: 'peer dep missing', log: true }, + ]; + + return BbPromise.fromCallback(cb => { + childProcess.exec(command, { + cwd: cwd, + maxBuffer: maxExecBufferSize, + encoding: 'utf8' + }, (err, stdout, stderr) => { + if (err) { + // Only exit with an error if we have critical npm errors for 2nd level inside + const errors = _.split(stderr, '\n'); + const failed = _.reduce(errors, (failed, error) => { + if (failed) { + return true; + } + return !_.isEmpty(error) && !_.some(ignoredNpmErrors, ignoredError => _.startsWith(error, `npm ERR! ${ignoredError.npmError}`)); + }, false); + + if (failed) { + return cb(err); + } + } + return cb(null, stdout); + }); + }) + .then(depJson => BbPromise.try(() => JSON.parse(depJson))); + } + + static _rebaseFileReferences(pathToPackageRoot, moduleVersion) { + if (/^file:[^/]{2}/.test(moduleVersion)) { + const filePath = _.replace(moduleVersion, /^file:/, ''); + return _.replace(`file:${pathToPackageRoot}/${filePath}`, /\\/g, '/'); + } + + return moduleVersion; + } + + static rebaseLockfile(pathToPackageRoot, lockfile) { + if (lockfile.version) { + lockfile.version = NPM._rebaseFileReferences(pathToPackageRoot, lockfile.version); + } + + if (lockfile.dependencies) { + _.forIn(lockfile.dependencies, lockedDependency => { + NPM.rebaseLockfile(pathToPackageRoot, lockedDependency); + }); + } + } + + static install(cwd, maxExecBufferSize) { + return BbPromise.fromCallback(cb => { + childProcess.exec('npm install', { + cwd: cwd, + maxBuffer: maxExecBufferSize, + encoding: 'utf8' + }, cb); + }) + .return(); + } + + static prune(cwd, maxExecBufferSize) { + return BbPromise.fromCallback(cb => { + childProcess.exec('npm prune', { + cwd: cwd, + maxBuffer: maxExecBufferSize, + encoding: 'utf8' + }, cb); + }) + .return(); + } +} + +module.exports = NPM; diff --git a/lib/packagers/npm.test.js b/lib/packagers/npm.test.js new file mode 100644 index 000000000..481f134ad --- /dev/null +++ b/lib/packagers/npm.test.js @@ -0,0 +1,214 @@ +'use strict'; +/** + * Unit tests for packagers/index + */ + +const _ = require('lodash'); +const BbPromise = require('bluebird'); +const chai = require('chai'); +const sinon = require('sinon'); +const mockery = require('mockery'); + +// Mocks +const childProcessMockFactory = require('../../tests/mocks/child_process.mock'); + +chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); + +const expect = chai.expect; + +describe('npm', () => { + let sandbox; + let npmModule; + + // Mocks + let childProcessMock; + + before(() => { + sandbox = sinon.sandbox.create(); + sandbox.usingPromise(BbPromise.Promise); + + childProcessMock = childProcessMockFactory.create(sandbox); + + mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); + mockery.registerMock('child_process', childProcessMock); + npmModule = require('./npm'); + }); + + after(() => { + mockery.disable(); + mockery.deregisterAll(); + + sandbox.restore(); + }); + + afterEach(() => { + sandbox.reset(); + }); + + it('should return "package-lock.json" as lockfile name', () => { + expect(npmModule.lockfileName).to.equal('package-lock.json'); + }); + + describe('install', () => { + it('should use npm install', () => { + childProcessMock.exec.yields(null, 'installed successfully', ''); + return expect(npmModule.install('myPath', 2000)).to.be.fulfilled + .then(result => { + expect(result).to.be.undefined; + expect(childProcessMock.exec).to.have.been.calledOnce; + expect(childProcessMock.exec).to.have.been.calledWithExactly( + 'npm install', + { + cwd: 'myPath', + encoding: 'utf8', + maxBuffer: 2000 + }, + sinon.match.any + ); + return null; + }); + }); + }); + + describe('prune', () => { + it('should use npm prune', () => { + childProcessMock.exec.yields(null, 'success', ''); + return expect(npmModule.prune('myPath', 2000)).to.be.fulfilled + .then(result => { + expect(result).to.be.undefined; + expect(childProcessMock.exec).to.have.been.calledOnce; + expect(childProcessMock.exec).to.have.been.calledWithExactly( + 'npm prune', + { + cwd: 'myPath', + encoding: 'utf8', + maxBuffer: 2000 + }, + sinon.match.any + ); + return null; + }); + }); + }); + + describe('getProdDependencies', () => { + it('should use npm ls', () => { + childProcessMock.exec.yields(null, '{}', ''); + return expect(npmModule.getProdDependencies('myPath', 10, 2000)).to.be.fulfilled + .then(result => { + expect(result).to.be.an('object').that.is.empty; + expect(childProcessMock.exec).to.have.been.calledOnce, + expect(childProcessMock.exec.firstCall).to.have.been.calledWith( + 'npm ls -prod -json -depth=10' + ); + return null; + }); + }); + + it('should default to depth 1', () => { + childProcessMock.exec.yields(null, '{}', ''); + return expect(npmModule.getProdDependencies('myPath')).to.be.fulfilled + .then(result => { + expect(result).to.be.an('object').that.is.empty; + expect(childProcessMock.exec).to.have.been.calledOnce, + expect(childProcessMock.exec.firstCall).to.have.been.calledWith( + 'npm ls -prod -json -depth=1' + ); + return null; + }); + }); + }); + + it('should reject if npm returns critical and minor errors', () => { + const stderr = 'ENOENT: No such file\nnpm ERR! extraneous: sinon@2.3.8 ./babel-dynamically-entries/node_modules/serverless-webpack/node_modules/sinon\n\n'; + childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr); + return expect(npmModule.getProdDependencies('myPath', 1)).to.be.rejectedWith('something went wrong') + .then(() => BbPromise.all([ + // npm ls and npm prune should have been called + expect(childProcessMock.exec).to.have.been.calledOnce, + expect(childProcessMock.exec.firstCall).to.have.been.calledWith( + 'npm ls -prod -json -depth=1' + ), + ])); + }); + + it('should ignore minor local NPM errors and log them', () => { + const stderr = _.join( + [ + 'npm ERR! extraneous: sinon@2.3.8 ./babel-dynamically-entries/node_modules/serverless-webpack/node_modules/sinon', + 'npm ERR! missing: internalpackage-1@1.0.0, required by internalpackage-2@1.0.0', + 'npm ERR! peer dep missing: sinon@2.3.8', + ], + '\n' + ); + const lsResult = { + version: '1.0.0', + problems: [ + 'npm ERR! extraneous: sinon@2.3.8 ./babel-dynamically-entries/node_modules/serverless-webpack/node_modules/sinon', + 'npm ERR! missing: internalpackage-1@1.0.0, required by internalpackage-2@1.0.0', + 'npm ERR! peer dep missing: sinon@2.3.8', + ], + dependencies: { + '@scoped/vendor': '1.0.0', + uuid: '^5.4.1', + bluebird: '^3.4.0' + } + }; + + childProcessMock.exec.yields(new Error('NPM error'), JSON.stringify(lsResult), stderr); + return expect(npmModule.getProdDependencies('myPath', 1)).to.be.fulfilled + .then(dependencies => BbPromise.all([ + // npm ls and npm prune should have been called + expect(childProcessMock.exec).to.have.been.calledOnce, + expect(childProcessMock.exec).to.have.been.calledWith( + 'npm ls -prod -json -depth=1' + ), + expect(dependencies).to.deep.equal(lsResult), + ])); + }); + + it('should rebase lock file references', () => { + const expectedLocalModule = 'file:../../locals/../../mymodule'; + const fakePackageLockJSON = { + name: 'test-service', + version: '1.0.0', + description: 'Packaged externals for test-service', + private: true, + dependencies: { + '@scoped/vendor': '1.0.0', + uuid: { + version: '^5.4.1' + }, + bluebird: { + version: '^3.4.0' + }, + localmodule: { + version: 'file:../../mymodule' + } + } + }; + const expectedPackageLockJSON = { + name: 'test-service', + version: '1.0.0', + description: 'Packaged externals for test-service', + private: true, + dependencies: { + '@scoped/vendor': '1.0.0', + uuid: { + version: '^5.4.1' + }, + bluebird: { + version: '^3.4.0' + }, + localmodule: { + version: expectedLocalModule + } + } + }; + + npmModule.rebaseLockfile('../../locals', fakePackageLockJSON); + expect(fakePackageLockJSON).to.deep.equal(expectedPackageLockJSON); + }); + +}); diff --git a/package.json b/package.json index 1b1678ab0..fe0ccaee2 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,13 @@ }, "homepage": "https://github.com/serverless-heaven/serverless-webpack#readme", "scripts": { - "test": "nyc ./node_modules/mocha/bin/_mocha tests/all -R spec --recursive", + "test": "nyc ./node_modules/mocha/bin/_mocha tests/all \"lib/**/*.test.js\" -R spec --recursive", "eslint": "node node_modules/eslint/bin/eslint.js --ext .js lib" }, "nyc": { "exclude": [ - "tests/**/*.*" + "tests/**/*.*", + "**/*.test.js" ], "reporter": [ "lcov", diff --git a/tests/packExternalModules.test.js b/tests/packExternalModules.test.js index 0c9a042db..5bb39e05f 100644 --- a/tests/packExternalModules.test.js +++ b/tests/packExternalModules.test.js @@ -9,7 +9,6 @@ const mockery = require('mockery'); const Serverless = require('serverless'); // Mocks -const childProcessMockFactory = require('./mocks/child_process.mock'); const fsExtraMockFactory = require('./mocks/fs-extra.mock'); const packageMock = require('./mocks/package.mock.json'); const packageLocalRefMock = require('./mocks/packageLocalRef.mock.json'); @@ -19,6 +18,20 @@ chai.use(require('sinon-chai')); const expect = chai.expect; +const packagerMockFactory = { + create(sandbox) { + const packagerMock = { + lockfileName: 'mocked-lock.json', + rebaseLockfile: sandbox.stub(), + getProdDependencies: sandbox.stub(), + install: sandbox.stub(), + prune: sandbox.stub() + }; + + return packagerMock; + } +}; + describe('packExternalModules', () => { let sandbox; let baseModule; @@ -26,7 +39,8 @@ describe('packExternalModules', () => { let module; // Mocks - let childProcessMock; + let packagerFactoryMock; + let packagerMock; let fsExtraMock; // Serverless stubs let writeFileSyncStub; @@ -34,14 +48,21 @@ describe('packExternalModules', () => { before(() => { sandbox = sinon.sandbox.create(); - sandbox.usingPromise(BbPromise); + sandbox.usingPromise(BbPromise.Promise); - childProcessMock = childProcessMockFactory.create(sandbox); + packagerMock = packagerMockFactory.create(sandbox); fsExtraMock = fsExtraMockFactory.create(sandbox); - mockery.enable({ warnOnUnregistered: false }); - mockery.registerMock('child_process', childProcessMock); + // Setup packager mocks + packagerFactoryMock = { + get: sinon.stub() + }; + packagerFactoryMock.get.withArgs('npm').returns(packagerMock); + packagerFactoryMock.get.throws(new Error('Packager not mocked')); + + mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); mockery.registerMock('fs-extra', fsExtraMock); + mockery.registerMock('./packagers', packagerFactoryMock); mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock); baseModule = require('../lib/packExternalModules'); Object.freeze(baseModule); @@ -50,6 +71,7 @@ describe('packExternalModules', () => { after(() => { mockery.disable(); mockery.deregisterAll(); + sandbox.restore(); }); beforeEach(() => { @@ -74,13 +96,11 @@ describe('packExternalModules', () => { afterEach(() => { // Reset all counters and restore all stubbed functions - writeFileSyncStub.reset(); - readFileSyncStub.reset(); - childProcessMock.exec.reset(); + writeFileSyncStub.restore(); + readFileSyncStub.restore(); fsExtraMock.pathExists.reset(); fsExtraMock.copy.reset(); sandbox.reset(); - sandbox.restore(); }); describe('packExternalModules()', () => { @@ -206,7 +226,7 @@ describe('packExternalModules', () => { return expect(module.packExternalModules()).to.be.fulfilled .then(() => BbPromise.all([ expect(fsExtraMock.copy).to.not.have.been.called, - expect(childProcessMock.exec).to.not.have.been.called, + expect(packagerFactoryMock.get).to.not.have.been.called, expect(writeFileSyncStub).to.not.have.been.called, ])); }); @@ -234,9 +254,9 @@ describe('packExternalModules', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(null, '{}', ''); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve({})); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = stats; return expect(module.packExternalModules()).to.be.fulfilled .then(() => BbPromise.all([ @@ -247,16 +267,9 @@ describe('packExternalModules', () => { // The modules should have been copied expect(fsExtraMock.copy).to.have.been.calledOnce, // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledThrice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ), - expect(childProcessMock.exec.thirdCall).to.have.been.calledWith( - 'npm prune' - ) + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, ])); }); @@ -301,33 +314,16 @@ describe('packExternalModules', () => { } } }; - const expectedPackageLockJSON = { - name: 'test-service', - version: '1.0.0', - description: 'Packaged externals for test-service', - private: true, - dependencies: { - '@scoped/vendor': '1.0.0', - uuid: { - version: '^5.4.1' - }, - bluebird: { - version: '^3.4.0' - }, - localmodule: { - version: expectedLocalModule - } - } - }; _.set(serverless, 'service.custom.webpackIncludeModules.packagePath', path.join('locals', 'package.json')); module.webpackOutputPath = 'outputPath'; readFileSyncStub.returns(fakePackageLockJSON); fsExtraMock.pathExists.yields(null, true); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(null, '{}', ''); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve({})); + packagerMock.rebaseLockfile.callsFake((pathToPackageRoot, lockfile) => lockfile); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = statsWithFileRef; sandbox.stub(process, 'cwd').returns(path.join('/my/Service/Path')); @@ -338,22 +334,20 @@ describe('packExternalModules', () => { // The module package JSON and the composite one should have been stored expect(writeFileSyncStub).to.have.been.calledThrice, expect(writeFileSyncStub.firstCall.args[1]).to.equal(JSON.stringify(expectedCompositePackageJSON, null, 2)), - expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageLockJSON, null, 2)), expect(writeFileSyncStub.thirdCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)), // The modules should have been copied expect(fsExtraMock.copy).to.have.been.calledOnce, + // Lock file rebase should have been called + expect(packagerMock.rebaseLockfile).to.have.been.calledOnce, + expect(packagerMock.rebaseLockfile).to.have.been.calledWith(sinon.match.any, sinon.match(fakePackageLockJSON)), // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledThrice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ), - expect(childProcessMock.exec.thirdCall).to.have.been.calledWith( - 'npm prune' - ) - ])); + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, + ])) + .finally(() => { + process.cwd.restore(); + }); }); it('should skip module copy for Google provider', () => { @@ -380,9 +374,9 @@ describe('packExternalModules', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(null, '{}', ''); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve({})); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = stats; return expect(module.packExternalModules()).to.be.fulfilled .then(() => BbPromise.all([ @@ -393,37 +387,34 @@ describe('packExternalModules', () => { // The modules should have been copied expect(fsExtraMock.copy).to.have.not.been.called, // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledTwice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ) + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.not.have.been.called, ])); }); - it('should reject if npm install fails', () => { + it('should reject if packager install fails', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(null, '{}', ''); - childProcessMock.exec.onSecondCall().yields(new Error('npm install failed')); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve({})); + packagerMock.install.callsFake(() => BbPromise.reject(new Error('npm install failed'))); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = stats; return expect(module.packExternalModules()).to.be.rejectedWith('npm install failed') .then(() => BbPromise.all([ // npm ls and npm install should have been called - expect(childProcessMock.exec).to.have.been.calledTwice, + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.not.have.been.called, ])); }); - it('should reject if npm returns a critical error', () => { - const stderr = 'ENOENT: No such file'; + it('should reject if packager returns a critical error', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr); + packagerMock.getProdDependencies.callsFake(() => BbPromise.reject(new Error('something went wrong'))); module.compileStats = stats; return expect(module.packExternalModules()).to.be.rejectedWith('something went wrong') .then(() => BbPromise.all([ @@ -432,119 +423,50 @@ describe('packExternalModules', () => { // The modules should have been copied expect(fsExtraMock.copy).to.not.have.been.called, // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledOnce, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.not.have.been.called, + expect(packagerMock.prune).to.not.have.been.called, ])); }); - it('should reject if npm returns critical and minor errors', () => { - const stderr = 'ENOENT: No such file\nnpm ERR! extraneous: sinon@2.3.8 ./babel-dynamically-entries/node_modules/serverless-webpack/node_modules/sinon\n\n'; + it('should not install modules if no external modules are reported', () => { module.webpackOutputPath = 'outputPath'; - fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr); - module.compileStats = stats; - return expect(module.packExternalModules()).to.be.rejectedWith('something went wrong') + packagerMock.getProdDependencies.returns(BbPromise.resolve()); + module.compileStats = noExtStats; + 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.not.have.been.called, // The modules should have been copied expect(fsExtraMock.copy).to.not.have.been.called, - // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledOnce, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), + // npm install and npm prune should not have been called + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.not.have.been.called, + expect(packagerMock.prune).to.not.have.been.called, ])); }); - it('should ignore minor local NPM errors and log them', () => { - const expectedCompositePackageJSON = { - name: 'test-service', - version: '1.0.0', - description: 'Packaged externals for test-service', - private: true, - dependencies: { - '@scoped/vendor': '1.0.0', - uuid: '^5.4.1', - bluebird: '^3.4.0' - } - }; - const expectedPackageJSON = { - dependencies: { - '@scoped/vendor': '1.0.0', - uuid: '^5.4.1', - bluebird: '^3.4.0' - } - }; - const stderr = _.join( - [ - 'npm ERR! extraneous: sinon@2.3.8 ./babel-dynamically-entries/node_modules/serverless-webpack/node_modules/sinon', - 'npm ERR! missing: internalpackage-1@1.0.0, required by internalpackage-2@1.0.0', - 'npm ERR! peer dep missing: sinon@2.3.8', - ], - '\n' - ); - const lsResult = { - version: '1.0.0', - problems: [ - 'npm ERR! extraneous: sinon@2.3.8 ./babel-dynamically-entries/node_modules/serverless-webpack/node_modules/sinon', - 'npm ERR! missing: internalpackage-1@1.0.0, required by internalpackage-2@1.0.0', - 'npm ERR! peer dep missing: sinon@2.3.8', - ], - dependencies: { - '@scoped/vendor': '1.0.0', - uuid: '^5.4.1', - bluebird: '^3.4.0' - } - }; - + it('should report ignored packager problems in verbose mode', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(new Error('NPM error'), JSON.stringify(lsResult), stderr); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve({ + problems: [ + 'Problem 1', + 'Problem 2' + ] + })); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = stats; 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.pathExists).to.have.been.calledOnce, - expect(fsExtraMock.copy).to.have.been.calledOnce, - // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledThrice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ), - expect(childProcessMock.exec.thirdCall).to.have.been.calledWith( - 'npm prune' - ) - ])); - }); - - it('should not install modules if no external modules are reported', () => { - module.webpackOutputPath = 'outputPath'; - fsExtraMock.copy.yields(); - childProcessMock.exec.yields(null, '{}', ''); - module.compileStats = noExtStats; - 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.not.have.been.called, - // The modules should have been copied - expect(fsExtraMock.copy).to.not.have.been.called, - // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledOnce, - ])); + .then(() => { + expect(packagerMock.getProdDependencies).to.have.been.calledOnce; + expect(serverless.cli.log).to.have.been.calledWith('=> Problem 1'); + expect(serverless.cli.log).to.have.been.calledWith('=> Problem 2'); + return null; + }); }); it('should install external modules when forced', () => { @@ -576,9 +498,9 @@ describe('packExternalModules', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(null, '{}', ''); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve({})); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = stats; return expect(module.packExternalModules()).to.be.fulfilled .then(() => BbPromise.all([ @@ -589,16 +511,9 @@ describe('packExternalModules', () => { // The modules should have been copied expect(fsExtraMock.copy).to.have.been.calledOnce, // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledThrice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ), - expect(childProcessMock.exec.thirdCall).to.have.been.calledWith( - 'npm prune' - ) + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, ])); }); @@ -631,9 +546,9 @@ describe('packExternalModules', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(null, '{}', ''); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve({})); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = stats; return expect(module.packExternalModules()).to.be.fulfilled .then(() => BbPromise.all([ @@ -644,16 +559,9 @@ describe('packExternalModules', () => { // The modules should have been copied expect(fsExtraMock.copy).to.have.been.calledOnce, // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledThrice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ), - expect(childProcessMock.exec.thirdCall).to.have.been.calledWith( - 'npm prune' - ) + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, ])); }); @@ -685,9 +593,9 @@ describe('packExternalModules', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(null, '{}', ''); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve({})); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = stats; return expect(module.packExternalModules()).to.be.fulfilled .then(() => BbPromise.all([ @@ -698,16 +606,9 @@ describe('packExternalModules', () => { // The modules should have been copied expect(fsExtraMock.copy).to.have.been.calledOnce, // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledThrice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ), - expect(childProcessMock.exec.thirdCall).to.have.been.calledWith( - 'npm prune' - ) + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, ])); }); @@ -734,29 +635,25 @@ describe('packExternalModules', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, true); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(null, '{}', ''); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + readFileSyncStub.returns({ info: 'lockfile' }); + packagerMock.rebaseLockfile.callsFake((pathToPackageRoot, lockfile) => lockfile); + packagerMock.getProdDependencies.returns(BbPromise.resolve({})); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = stats; 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).to.have.been.calledThrice, expect(writeFileSyncStub.firstCall.args[1]).to.equal(JSON.stringify(expectedCompositePackageJSON, null, 2)), - expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)), + expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify({ info: 'lockfile' }, null, 2)), + expect(writeFileSyncStub.thirdCall.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(childProcessMock.exec).to.have.been.calledThrice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ), - expect(childProcessMock.exec.thirdCall).to.have.been.calledWith( - 'npm prune' - ) + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, ])); }); @@ -784,9 +681,9 @@ describe('packExternalModules', () => { readFileSyncStub.throws(new Error('Failed to read package-lock.json')); fsExtraMock.pathExists.yields(null, true); fsExtraMock.copy.onFirstCall().yields(); - childProcessMock.exec.onFirstCall().yields(null, '{}', ''); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve({})); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = stats; return expect(module.packExternalModules()).to.be.fulfilled .then(() => BbPromise.all([ @@ -797,16 +694,9 @@ describe('packExternalModules', () => { // The modules should have been copied expect(fsExtraMock.copy).to.have.been.calledOnce, // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledThrice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ), - expect(childProcessMock.exec.thirdCall).to.have.been.calledWith( - 'npm prune' - ) + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, ])); }); @@ -864,9 +754,9 @@ describe('packExternalModules', () => { module.webpackOutputPath = 'outputPath'; fsExtraMock.pathExists.yields(null, false); fsExtraMock.copy.yields(); - childProcessMock.exec.onFirstCall().yields(null, JSON.stringify(dependencyGraph), ''); - childProcessMock.exec.onSecondCall().yields(null, '', ''); - childProcessMock.exec.onThirdCall().yields(); + packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph)); + packagerMock.install.returns(BbPromise.resolve()); + packagerMock.prune.returns(BbPromise.resolve()); module.compileStats = peerDepStats; return expect(module.packExternalModules()).to.be.fulfilled .then(() => BbPromise.all([ @@ -877,16 +767,9 @@ describe('packExternalModules', () => { // The modules should have been copied expect(fsExtraMock.copy).to.have.been.calledOnce, // npm ls and npm prune should have been called - expect(childProcessMock.exec).to.have.been.calledThrice, - expect(childProcessMock.exec.firstCall).to.have.been.calledWith( - 'npm ls -prod -json -depth=1' - ), - expect(childProcessMock.exec.secondCall).to.have.been.calledWith( - 'npm install' - ), - expect(childProcessMock.exec.thirdCall).to.have.been.calledWith( - 'npm prune' - ) + expect(packagerMock.getProdDependencies).to.have.been.calledOnce, + expect(packagerMock.install).to.have.been.calledOnce, + expect(packagerMock.prune).to.have.been.calledOnce, ])); }); });