You've already forked serverless-webpack
mirror of
https://github.com/encounter/serverless-webpack.git
synced 2026-03-30 11:37:58 -07:00
68104f6aa5
* Add and include 2nd level peer dependencies * Only inspect peers for direct dependencies * Added unit tests for peer dependencies
536 lines
20 KiB
JavaScript
536 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
const BbPromise = require('bluebird');
|
|
const _ = require('lodash');
|
|
const path = require('path');
|
|
const chai = require('chai');
|
|
const sinon = require('sinon');
|
|
const mockery = require('mockery');
|
|
const Serverless = require('serverless');
|
|
|
|
// Mocks
|
|
const npmMockFactory = require('./mocks/npm-programmatic.mock');
|
|
const childProcessMockFactory = require('./mocks/child_process.mock');
|
|
const fsExtraMockFactory = require('./mocks/fs-extra.mock');
|
|
const packageMock = require('./mocks/package.mock.json');
|
|
|
|
chai.use(require('chai-as-promised'));
|
|
chai.use(require('sinon-chai'));
|
|
|
|
const expect = chai.expect;
|
|
|
|
describe('packExternalModules', () => {
|
|
let sandbox;
|
|
let baseModule;
|
|
let serverless;
|
|
let module;
|
|
|
|
// Mocks
|
|
let npmMock;
|
|
let childProcessMock;
|
|
let fsExtraMock;
|
|
// Serverless stubs
|
|
let writeFileSyncStub;
|
|
|
|
before(() => {
|
|
sandbox = sinon.sandbox.create();
|
|
sandbox.usingPromise(BbPromise);
|
|
|
|
npmMock = npmMockFactory.create(sandbox);
|
|
childProcessMock = childProcessMockFactory.create(sandbox);
|
|
fsExtraMock = fsExtraMockFactory.create(sandbox);
|
|
|
|
mockery.enable({ warnOnUnregistered: false });
|
|
mockery.registerMock('npm-programmatic', npmMock);
|
|
mockery.registerMock('child_process', childProcessMock);
|
|
mockery.registerMock('fs-extra', fsExtraMock);
|
|
mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock);
|
|
baseModule = require('../lib/packExternalModules');
|
|
Object.freeze(baseModule);
|
|
});
|
|
|
|
after(() => {
|
|
mockery.disable();
|
|
mockery.deregisterAll();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
serverless = new Serverless();
|
|
serverless.cli = {
|
|
log: sandbox.stub(),
|
|
consoleLog: sandbox.stub()
|
|
};
|
|
|
|
writeFileSyncStub = sandbox.stub(serverless.utils, 'writeFileSync');
|
|
_.set(serverless, 'service.custom.webpackIncludeModules', true);
|
|
|
|
module = _.assign({
|
|
serverless,
|
|
options: {
|
|
verbose: true
|
|
},
|
|
}, baseModule);
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Reset all counters and restore all stubbed functions
|
|
writeFileSyncStub.reset();
|
|
childProcessMock.exec.reset();
|
|
fsExtraMock.copy.reset();
|
|
npmMock.install.reset();
|
|
sandbox.reset();
|
|
sandbox.restore();
|
|
});
|
|
|
|
describe('packExternalModules()', () => {
|
|
// Test data
|
|
const stats = {
|
|
stats: [
|
|
{
|
|
compilation: {
|
|
chunks: [
|
|
{
|
|
modules: [
|
|
{
|
|
identifier: _.constant('"crypto"')
|
|
},
|
|
{
|
|
identifier: _.constant('"uuid/v4"')
|
|
},
|
|
{
|
|
identifier: _.constant('external "eslint"')
|
|
},
|
|
{
|
|
identifier: _.constant('"mockery"')
|
|
},
|
|
{
|
|
identifier: _.constant('"@scoped/vendor/module1"')
|
|
},
|
|
{
|
|
identifier: _.constant('external "@scoped/vendor/module2"')
|
|
},
|
|
{
|
|
identifier: _.constant('external "uuid/v4"')
|
|
},
|
|
{
|
|
identifier: _.constant('external "bluebird"')
|
|
},
|
|
]
|
|
}
|
|
],
|
|
compiler: {
|
|
outputPath: '/my/Service/Path/.webpack/service'
|
|
}
|
|
}
|
|
}
|
|
]
|
|
};
|
|
const noExtStats = {
|
|
stats: [
|
|
{
|
|
compilation: {
|
|
chunks: [
|
|
{
|
|
modules: [
|
|
{
|
|
identifier: _.constant('"crypto"')
|
|
},
|
|
{
|
|
identifier: _.constant('"uuid/v4"')
|
|
},
|
|
{
|
|
identifier: _.constant('"mockery"')
|
|
},
|
|
{
|
|
identifier: _.constant('"@scoped/vendor/module1"')
|
|
},
|
|
]
|
|
}
|
|
],
|
|
compiler: {
|
|
outputPath: '/my/Service/Path/.webpack/service'
|
|
}
|
|
}
|
|
}
|
|
]
|
|
};
|
|
|
|
it('should do nothing if webpackIncludeModules is not set', () => {
|
|
_.unset(serverless, 'service.custom.webpackIncludeModules');
|
|
return expect(module.packExternalModules({ stats: [] })).to.eventually.deep.equal({ stats: [] })
|
|
.then(() => BbPromise.all([
|
|
expect(npmMock.install).to.not.have.been.called,
|
|
expect(fsExtraMock.copy).to.not.have.been.called,
|
|
expect(childProcessMock.exec).to.not.have.been.called,
|
|
expect(writeFileSyncStub).to.not.have.been.called,
|
|
]));
|
|
});
|
|
|
|
it('should install external modules', () => {
|
|
const expectedPackageJSON = {
|
|
dependencies: {
|
|
'@scoped/vendor': '1.0.0',
|
|
uuid: '^5.4.1',
|
|
bluebird: '^3.4.0'
|
|
}
|
|
};
|
|
|
|
module.webpackOutputPath = 'outputPath';
|
|
npmMock.install.returns(BbPromise.resolve());
|
|
fsExtraMock.copy.yields();
|
|
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
|
|
childProcessMock.exec.onSecondCall().yields();
|
|
return expect(module.packExternalModules(stats)).to.be.fulfilled
|
|
.then(() => BbPromise.all([
|
|
// npm install should have been called with all externals from the package mock
|
|
expect(npmMock.install).to.have.been.calledOnce,
|
|
expect(npmMock.install).to.have.been.calledWithExactly([
|
|
'@scoped/vendor@1.0.0',
|
|
'uuid@^5.4.1',
|
|
'bluebird@^3.4.0'
|
|
],
|
|
{
|
|
cwd: path.join('outputPath', 'dependencies'),
|
|
maxBuffer: 204800,
|
|
save: true
|
|
}),
|
|
// 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('{}'),
|
|
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
|
|
// The modules should have been copied
|
|
expect(fsExtraMock.copy).to.have.been.calledOnce,
|
|
// npm ls and npm prune should have been called
|
|
expect(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 prune'
|
|
)
|
|
]));
|
|
});
|
|
|
|
it('should reject if npm install fails', () => {
|
|
module.webpackOutputPath = 'outputPath';
|
|
npmMock.install.returns(BbPromise.reject(new Error('npm install failed')));
|
|
fsExtraMock.copy.yields();
|
|
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
|
|
childProcessMock.exec.onSecondCall().yields();
|
|
return expect(module.packExternalModules(stats)).to.be.rejectedWith('npm install failed')
|
|
.then(() => BbPromise.all([
|
|
// npm install should have been called with all externals from the package mock
|
|
expect(npmMock.install).to.have.been.calledOnce,
|
|
// npm ls and npm prune should have been called
|
|
expect(childProcessMock.exec).to.have.been.calledOnce,
|
|
]));
|
|
});
|
|
|
|
it('should reject if npm returns a critical error', () => {
|
|
const stderr = 'ENOENT: No such file';
|
|
module.webpackOutputPath = 'outputPath';
|
|
npmMock.install.returns(BbPromise.resolve());
|
|
fsExtraMock.copy.yields();
|
|
childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr);
|
|
return expect(module.packExternalModules(stats)).to.be.rejectedWith('something went wrong')
|
|
.then(() => BbPromise.all([
|
|
expect(npmMock.install).to.not.have.been.called,
|
|
// 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'
|
|
),
|
|
]));
|
|
});
|
|
|
|
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';
|
|
module.webpackOutputPath = 'outputPath';
|
|
npmMock.install.returns(BbPromise.resolve());
|
|
fsExtraMock.copy.yields();
|
|
childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr);
|
|
return expect(module.packExternalModules(stats)).to.be.rejectedWith('something went wrong')
|
|
.then(() => BbPromise.all([
|
|
expect(npmMock.install).to.not.have.been.called,
|
|
// 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'
|
|
),
|
|
]));
|
|
});
|
|
|
|
it('should ignore minor local NPM errors and log them', () => {
|
|
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'
|
|
}
|
|
};
|
|
|
|
module.webpackOutputPath = 'outputPath';
|
|
npmMock.install.returns(BbPromise.resolve());
|
|
fsExtraMock.copy.yields();
|
|
childProcessMock.exec.onFirstCall().yields(new Error('NPM error'), JSON.stringify(lsResult), stderr);
|
|
childProcessMock.exec.onSecondCall().yields();
|
|
return expect(module.packExternalModules(stats)).to.be.fulfilled
|
|
.then(() => BbPromise.all([
|
|
// npm install should have been called with all externals from the package mock
|
|
expect(npmMock.install).to.have.been.calledOnce,
|
|
expect(npmMock.install).to.have.been.calledWithExactly([
|
|
'@scoped/vendor@1.0.0',
|
|
'uuid@^5.4.1',
|
|
'bluebird@^3.4.0'
|
|
],
|
|
{
|
|
cwd: path.join('outputPath', 'dependencies'),
|
|
maxBuffer: 204800,
|
|
save: true
|
|
}),
|
|
// 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('{}'),
|
|
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
|
|
// The modules should have been copied
|
|
expect(fsExtraMock.copy).to.have.been.calledOnce,
|
|
// npm ls and npm prune should have been called
|
|
expect(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 prune'
|
|
)
|
|
]));
|
|
});
|
|
|
|
it('should not install modules if no external modules are reported', () => {
|
|
module.webpackOutputPath = 'outputPath';
|
|
npmMock.install.returns(BbPromise.resolve());
|
|
fsExtraMock.copy.yields();
|
|
childProcessMock.exec.yields(null, '{}', '');
|
|
return expect(module.packExternalModules(noExtStats)).to.be.fulfilled
|
|
.then(stats => BbPromise.all([
|
|
expect(stats).to.deep.equal(noExtStats),
|
|
expect(npmMock.install).to.not.have.been.called,
|
|
// 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,
|
|
]));
|
|
});
|
|
|
|
it('should install external modules when forced', () => {
|
|
const expectedPackageJSON = {
|
|
dependencies: {
|
|
'@scoped/vendor': '1.0.0',
|
|
uuid: '^5.4.1',
|
|
bluebird: '^3.4.0',
|
|
pg: '^4.3.5'
|
|
}
|
|
};
|
|
serverless.service.custom = {
|
|
webpackIncludeModules: {
|
|
forceInclude: ['pg']
|
|
}
|
|
};
|
|
module.webpackOutputPath = 'outputPath';
|
|
npmMock.install.returns(BbPromise.resolve());
|
|
fsExtraMock.copy.yields();
|
|
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
|
|
childProcessMock.exec.onSecondCall().yields();
|
|
return expect(module.packExternalModules(stats)).to.be.fulfilled
|
|
.then(() => BbPromise.all([
|
|
// npm install should have been called with all externals from the package mock
|
|
expect(npmMock.install).to.have.been.calledOnce,
|
|
expect(npmMock.install).to.have.been.calledWithExactly([
|
|
'@scoped/vendor@1.0.0',
|
|
'uuid@^5.4.1',
|
|
'bluebird@^3.4.0',
|
|
'pg@^4.3.5'
|
|
],
|
|
{
|
|
cwd: path.join('outputPath', 'dependencies'),
|
|
maxBuffer: 204800,
|
|
save: true
|
|
}),
|
|
// 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('{}'),
|
|
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
|
|
// The modules should have been copied
|
|
expect(fsExtraMock.copy).to.have.been.calledOnce,
|
|
// npm ls and npm prune should have been called
|
|
expect(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 prune'
|
|
)
|
|
]));
|
|
});
|
|
|
|
it('should add forced external modules without version when not in production dependencies', () => {
|
|
const expectedPackageJSON = {
|
|
dependencies: {
|
|
'@scoped/vendor': '1.0.0',
|
|
uuid: '^5.4.1',
|
|
bluebird: '^3.4.0',
|
|
'not-in-prod-deps': ''
|
|
}
|
|
};
|
|
serverless.service.custom = {
|
|
webpackIncludeModules: {
|
|
forceInclude: ['not-in-prod-deps']
|
|
}
|
|
};
|
|
module.webpackOutputPath = 'outputPath';
|
|
npmMock.install.returns(BbPromise.resolve());
|
|
fsExtraMock.copy.yields();
|
|
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
|
|
childProcessMock.exec.onSecondCall().yields();
|
|
return expect(module.packExternalModules(stats)).to.be.fulfilled
|
|
.then(() => BbPromise.all([
|
|
// npm install should have been called with all externals from the package mock
|
|
expect(npmMock.install).to.have.been.calledOnce,
|
|
expect(npmMock.install).to.have.been.calledWithExactly([
|
|
'@scoped/vendor@1.0.0',
|
|
'uuid@^5.4.1',
|
|
'bluebird@^3.4.0',
|
|
'not-in-prod-deps'
|
|
],
|
|
{
|
|
cwd: path.join('outputPath', 'dependencies'),
|
|
maxBuffer: 204800,
|
|
save: true
|
|
}),
|
|
// 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('{}'),
|
|
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
|
|
// The modules should have been copied
|
|
expect(fsExtraMock.copy).to.have.been.calledOnce,
|
|
// npm ls and npm prune should have been called
|
|
expect(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 prune'
|
|
)
|
|
]));
|
|
});
|
|
|
|
describe('peer dependencies', () => {
|
|
before(() => {
|
|
const peerDepPackageJson = require('./data/package-peerdeps.json');
|
|
mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
|
|
mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson);
|
|
// Mock request-promise package.json
|
|
const rpPackageJson = require('./data/rp-package.json');
|
|
const rpPackagePath = path.join(
|
|
process.cwd(),
|
|
'node_modules',
|
|
'request-promise',
|
|
'package.json'
|
|
);
|
|
mockery.registerMock(rpPackagePath, rpPackageJson);
|
|
});
|
|
|
|
after(() => {
|
|
mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
|
|
mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock);
|
|
const rpPackagePath = path.join(
|
|
process.cwd(),
|
|
'node_modules',
|
|
'request-promise',
|
|
'package.json'
|
|
);
|
|
mockery.deregisterMock(rpPackagePath);
|
|
});
|
|
|
|
it('should install external peer dependencies', () => {
|
|
const expectedPackageJSON = {
|
|
dependencies: {
|
|
bluebird: '^3.5.0',
|
|
'request-promise': '^4.2.1',
|
|
request: '^2.82.0'
|
|
}
|
|
};
|
|
|
|
const dependencyGraph = require('./data/npm-ls-peerdeps.json');
|
|
const peerDepStats = require('./data/stats-peerdeps.js');
|
|
|
|
module.webpackOutputPath = 'outputPath';
|
|
npmMock.install.returns(BbPromise.resolve());
|
|
fsExtraMock.copy.yields();
|
|
childProcessMock.exec.onFirstCall().yields(null, JSON.stringify(dependencyGraph), '');
|
|
childProcessMock.exec.onSecondCall().yields();
|
|
return expect(module.packExternalModules(peerDepStats)).to.be.fulfilled
|
|
.then(() => BbPromise.all([
|
|
// npm install should have been called with all externals from the package mock
|
|
// and should additionally add request as peer dependency of request-promise
|
|
expect(npmMock.install).to.have.been.calledOnce,
|
|
expect(npmMock.install).to.have.been.calledWithExactly([
|
|
'bluebird@^3.5.0',
|
|
'request-promise@^4.2.1',
|
|
'request@^2.82.0'
|
|
],
|
|
{
|
|
cwd: path.join('outputPath', 'dependencies'),
|
|
maxBuffer: 204800,
|
|
save: true
|
|
}),
|
|
// 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('{}'),
|
|
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
|
|
// The modules should have been copied
|
|
expect(fsExtraMock.copy).to.have.been.calledOnce,
|
|
// npm ls and npm prune should have been called
|
|
expect(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 prune'
|
|
)
|
|
]));
|
|
});
|
|
});
|
|
});
|
|
});
|