Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make serverless-webpack more extensible #254

Merged
merged 7 commits into from
Oct 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,7 @@ WebPack's [Tree-Shaking][link-webpack-tree] optimization.

## Recent improvements

* Support for individual packaging and optimization
* Integrate with `serverless invoke local` (including watch mode)
* Stabilized package handling
* Improved compatibility with other plugins
* Updated examples
* Improved extensibility for plugin authors (see _For Developers_ section)

For the complete release notes see the end of this document.

Expand Down Expand Up @@ -406,6 +402,59 @@ Plugin commands are supported by the following providers. ⁇ indicates that com
| invoke local | ✔︎ | ✔︎ | ⁇ | ⁇ |
| invoke local --watch | ✔︎ | ✔︎ | ⁇ | ⁇ |

## For developers

The plugin exposes a complete lifecycle model that can be hooked by other plugins to extend
the functionality of the plugin or add additional actions.

### The event lifecycles and their hookable events (H)

All events (H) can be hooked by a plugin.

```
-> webpack:validate
-> webpack:validate:validate (H)
-> webpack:compile
-> webpack:compile:compile (H)
-> webpack:package
-> webpack:package:packExternalModules (H)
-> webpack:package:packageModules (H)
```

### Integration of the lifecycles into the command invocations and hooks

The following list shows all lifecycles that are invoked/started by the
plugin when running a command or invoked by a hook.

```
-> before:package:createDeploymentArtifacts
-> webpack:validate
-> webpack:compile
-> webpack:package

-> before:deploy:function:packageFunction
-> webpack:validate
-> webpack:compile
-> webpack:package

-> before:invoke:local:invoke
-> webpack:validate
-> webpack:compile

-> webpack
-> webpack:validate
-> webpack:compile
-> webpack:package

-> before:offline:start
-> webpack:validate
-> webpack:compile

-> before:offline:start:init
-> webpack:validate
-> webpack:compile
```

## Release Notes

* 3.1.2
Expand Down
71 changes: 36 additions & 35 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ class ServerlessWebpack {
webpack: {
usage: 'Bundle with Webpack',
lifecycleEvents: [
'validate',
'compile',
'webpack'
],
options: {
out: {
Expand All @@ -58,22 +57,23 @@ class ServerlessWebpack {
},
},
commands: {
invoke: {
usage: 'Run a function locally from the webpack output bundle',
validate: {
type: 'entrypoint',
lifecycleEvents: [
'invoke',
'validate',
],
},
watch: {
usage: 'Run a function from the webpack output bundle every time the source is changed',
compile: {
type: 'entrypoint',
lifecycleEvents: [
'watch',
'compile',
],
},
serve: {
usage: 'Simulate the API Gateway and serves lambdas locally',
package: {
type: 'entrypoint',
lifecycleEvents: [
'serve',
'packExternalModules',
'packageModules'
],
},
},
Expand All @@ -82,23 +82,21 @@ class ServerlessWebpack {

this.hooks = {
'before:package:createDeploymentArtifacts': () => BbPromise.bind(this)
.then(this.validate)
.then(this.compile)
.then(this.packExternalModules)
.then(this.packageModules),
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
.then(() => this.serverless.pluginManager.spawn('webpack:package')),

'after:package:createDeploymentArtifacts': () => BbPromise.bind(this)
.then(this.cleanup),

'before:deploy:function:packageFunction': () => BbPromise.bind(this)
.then(this.validate)
.then(this.compile)
.then(this.packExternalModules)
.then(this.packageModules),
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
.then(() => this.serverless.pluginManager.spawn('webpack:package')),

'before:invoke:local:invoke': () => BbPromise.bind(this)
.then(this.validate)
.then(this.compile)
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
.then(this.prepareLocalInvoke),

'after:invoke:local:invoke': () => BbPromise.bind(this)
Expand All @@ -109,31 +107,34 @@ class ServerlessWebpack {
return BbPromise.resolve();
}),

'webpack:validate': () => BbPromise.bind(this)
.then(this.validate),
'webpack:webpack': () => BbPromise.bind(this)
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
.then(() => this.serverless.pluginManager.spawn('webpack:package')),

'webpack:compile': () => BbPromise.bind(this)
.then(this.compile)
.then(this.packExternalModules)
.then(this.packageModules),
/*
* Internal webpack events (can be hooked by plugins)
*/
'webpack:validate:validate': () => BbPromise.bind(this)
.then(this.validate),

'webpack:invoke:invoke': () => BbPromise.bind(this)
.then(() => BbPromise.reject(new this.serverless.classes.Error('Use "serverless invoke local" instead.'))),
'webpack:compile:compile': () => BbPromise.bind(this)
.then(this.compile),

'webpack:watch:watch': () => BbPromise.bind(this)
.then(() => BbPromise.reject(new this.serverless.classes.Error('Use "serverless invoke local --watch" instead.'))),
'webpack:package:packExternalModules': () => BbPromise.bind(this)
.then(this.packExternalModules),

'webpack:serve:serve': () => BbPromise.bind(this)
.then(() => BbPromise.reject(new this.serverless.classes.Error('serve has been removed. Use serverless-offline instead.'))),
'webpack:package:packageModules': () => BbPromise.bind(this)
.then(this.packageModules),

'before:offline:start': () => BbPromise.bind(this)
.then(this.prepareOfflineInvoke)
.then(this.compile)
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
.then(this.wpwatch),

'before:offline:start:init': () => BbPromise.bind(this)
.then(this.prepareOfflineInvoke)
.then(this.compile)
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
.then(this.wpwatch),

};
Expand Down
3 changes: 2 additions & 1 deletion lib/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ module.exports = {
});

this.compileOutputPaths = compileOutputPaths;
this.compileStats = stats;

return stats;
return BbPromise.resolve();
});
},
};
10 changes: 6 additions & 4 deletions lib/packExternalModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,17 @@ module.exports = {
* This will utilize the npm cache at its best and give us the needed results
* and performance.
*/
packExternalModules(stats) {
packExternalModules() {

const stats = this.compileStats;

const includes = (
this.serverless.service.custom &&
this.serverless.service.custom.webpackIncludeModules
);

if (!includes) {
return BbPromise.resolve(stats);
return BbPromise.resolve();
}

// Read plugin configuration
Expand Down Expand Up @@ -231,7 +233,7 @@ module.exports = {
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(stats);
return BbPromise.resolve();
}

// (1.a) Install all needed modules
Expand Down Expand Up @@ -304,7 +306,7 @@ module.exports = {
.tap(() => this.options.verbose && this.serverless.cli.log(`Prune: ${modulePath} [${_.now() - startPrune} ms]`));
});
})
.return(stats);
.return();
});
}
};
4 changes: 3 additions & 1 deletion lib/packageModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ function zip(directory, name) {
}

module.exports = {
packageModules(stats) {
packageModules() {
const stats = this.compileStats;

return BbPromise.mapSeries(stats.stats, (compileStats, index) => {
const entryFunction = _.get(this.entryFunctions, index, {});
const filename = `${entryFunction.funcName || this.serverless.service.getServiceObject().name}.zip`;
Expand Down
2 changes: 1 addition & 1 deletion lib/prepareOfflineInvoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
// Use service packaging for compile
_.set(this.serverless, 'service.package.individually', false);

return this.validate()
return this.serverless.pluginManager.spawn('webpack:validate')
.then(() => {
// Set offline location automatically if not set manually
if (!this.options.location && !_.get(this.serverless, 'service.custom.serverless-offline.location')) {
Expand Down
42 changes: 27 additions & 15 deletions tests/packExternalModules.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ describe('packExternalModules', () => {

it('should do nothing if webpackIncludeModules is not set', () => {
_.unset(serverless, 'service.custom.webpackIncludeModules');
return expect(module.packExternalModules({ stats: [] })).to.eventually.deep.equal({ stats: [] })
module.compileStats = { stats: [] };
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,
Expand Down Expand Up @@ -188,7 +189,8 @@ describe('packExternalModules', () => {
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
childProcessMock.exec.onSecondCall().yields(null, '', '');
childProcessMock.exec.onThirdCall().yields();
return expect(module.packExternalModules(stats)).to.be.fulfilled
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,
Expand Down Expand Up @@ -217,7 +219,8 @@ describe('packExternalModules', () => {
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
childProcessMock.exec.onSecondCall().yields(new Error('npm install failed'));
childProcessMock.exec.onThirdCall().yields();
return expect(module.packExternalModules(stats)).to.be.rejectedWith('npm install failed')
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,
Expand All @@ -230,7 +233,8 @@ describe('packExternalModules', () => {
fsExtraMock.pathExists.yields(null, false);
fsExtraMock.copy.yields();
childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr);
return expect(module.packExternalModules(stats)).to.be.rejectedWith('something went wrong')
module.compileStats = stats;
return expect(module.packExternalModules()).to.be.rejectedWith('something went wrong')
.then(() => BbPromise.all([
// The module package JSON and the composite one should have been stored
expect(writeFileSyncStub).to.not.have.been.called,
Expand All @@ -250,7 +254,8 @@ describe('packExternalModules', () => {
fsExtraMock.pathExists.yields(null, false);
fsExtraMock.copy.yields();
childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr);
return expect(module.packExternalModules(stats)).to.be.rejectedWith('something went wrong')
module.compileStats = stats;
return expect(module.packExternalModules()).to.be.rejectedWith('something went wrong')
.then(() => BbPromise.all([
// The module package JSON and the composite one should have been stored
expect(writeFileSyncStub).to.not.have.been.called,
Expand Down Expand Up @@ -311,7 +316,8 @@ describe('packExternalModules', () => {
childProcessMock.exec.onFirstCall().yields(new Error('NPM error'), JSON.stringify(lsResult), stderr);
childProcessMock.exec.onSecondCall().yields(null, '', '');
childProcessMock.exec.onThirdCall().yields();
return expect(module.packExternalModules(stats)).to.be.fulfilled
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,
Expand All @@ -338,9 +344,9 @@ describe('packExternalModules', () => {
module.webpackOutputPath = 'outputPath';
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),
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
Expand Down Expand Up @@ -382,7 +388,8 @@ describe('packExternalModules', () => {
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
childProcessMock.exec.onSecondCall().yields(null, '', '');
childProcessMock.exec.onThirdCall().yields();
return expect(module.packExternalModules(stats)).to.be.fulfilled
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,
Expand Down Expand Up @@ -436,7 +443,8 @@ describe('packExternalModules', () => {
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
childProcessMock.exec.onSecondCall().yields(null, '', '');
childProcessMock.exec.onThirdCall().yields();
return expect(module.packExternalModules(stats)).to.be.fulfilled
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,
Expand Down Expand Up @@ -489,7 +497,8 @@ describe('packExternalModules', () => {
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
childProcessMock.exec.onSecondCall().yields(null, '', '');
childProcessMock.exec.onThirdCall().yields();
return expect(module.packExternalModules(stats)).to.be.fulfilled
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,
Expand Down Expand Up @@ -537,7 +546,8 @@ describe('packExternalModules', () => {
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
childProcessMock.exec.onSecondCall().yields(null, '', '');
childProcessMock.exec.onThirdCall().yields();
return expect(module.packExternalModules(stats)).to.be.fulfilled
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,
Expand Down Expand Up @@ -589,7 +599,8 @@ describe('packExternalModules', () => {
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
childProcessMock.exec.onSecondCall().yields(null, '', '');
childProcessMock.exec.onThirdCall().yields();
return expect(module.packExternalModules(stats)).to.be.fulfilled
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,
Expand Down Expand Up @@ -671,7 +682,8 @@ describe('packExternalModules', () => {
childProcessMock.exec.onFirstCall().yields(null, JSON.stringify(dependencyGraph), '');
childProcessMock.exec.onSecondCall().yields(null, '', '');
childProcessMock.exec.onThirdCall().yields();
return expect(module.packExternalModules(peerDepStats)).to.be.fulfilled
module.compileStats = peerDepStats;
return expect(module.packExternalModules()).to.be.fulfilled
.then(() => BbPromise.all([
// The module package JSON and the composite one should have been stored
expect(writeFileSyncStub).to.have.been.calledTwice,
Expand Down
Loading