From fee494763fd8ebf1593c12f3991cf80f9ac86284 Mon Sep 17 00:00:00 2001 From: Frank Schmid Date: Wed, 28 Feb 2018 18:49:19 +0100 Subject: [PATCH] Abstract packager into own interface Extended pack external unit tests Added unit tests for npm packager Adapted unit tests Exclude test.js files from nyc statistics Added unit test for packager factory Extract optional lock file adaption Fixed ESLint ESLint fixes Moved npm to a separate packager class. --- .npmignore | 1 + lib/packExternalModules.js | 268 +++++++++------------ lib/packagers/index.js | 38 +++ lib/packagers/index.test.js | 68 ++++++ lib/packagers/npm.js | 95 ++++++++ lib/packagers/npm.test.js | 214 +++++++++++++++++ package.json | 5 +- tests/packExternalModules.test.js | 383 +++++++++++------------------- 8 files changed, 663 insertions(+), 409 deletions(-) create mode 100644 lib/packagers/index.js create mode 100644 lib/packagers/index.test.js create mode 100644 lib/packagers/npm.js create mode 100644 lib/packagers/npm.test.js 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, ])); }); });