Skip to content

Commit 5c9f14d

Browse files
seriouscoderoneHyperBrain
authored andcommitted
Make serverless-webpack more extensible (#254)
* Make serverless-webpack more extensible Use PluginManager to make serverless-webpack more extensible Following the advice from serverless blog on Advanced Plugin Development. https://serverless.com/blog/advanced-plugin-development-extending-the-core-lifecycle/ * use sublifecycle with webpack command * Use new lifecycles and adapt unit tests * Fixed unit test after merge * Use new events everywhere * Added section to README
1 parent 8517bbc commit 5c9f14d

File tree

8 files changed

+142
-69
lines changed

8 files changed

+142
-69
lines changed

README.md

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@ WebPack's [Tree-Shaking][link-webpack-tree] optimization.
2626

2727
## Recent improvements
2828

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

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

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

405+
## For developers
406+
407+
The plugin exposes a complete lifecycle model that can be hooked by other plugins to extend
408+
the functionality of the plugin or add additional actions.
409+
410+
### The event lifecycles and their hookable events (H)
411+
412+
All events (H) can be hooked by a plugin.
413+
414+
```
415+
-> webpack:validate
416+
-> webpack:validate:validate (H)
417+
-> webpack:compile
418+
-> webpack:compile:compile (H)
419+
-> webpack:package
420+
-> webpack:package:packExternalModules (H)
421+
-> webpack:package:packageModules (H)
422+
```
423+
424+
### Integration of the lifecycles into the command invocations and hooks
425+
426+
The following list shows all lifecycles that are invoked/started by the
427+
plugin when running a command or invoked by a hook.
428+
429+
```
430+
-> before:package:createDeploymentArtifacts
431+
-> webpack:validate
432+
-> webpack:compile
433+
-> webpack:package
434+
435+
-> before:deploy:function:packageFunction
436+
-> webpack:validate
437+
-> webpack:compile
438+
-> webpack:package
439+
440+
-> before:invoke:local:invoke
441+
-> webpack:validate
442+
-> webpack:compile
443+
444+
-> webpack
445+
-> webpack:validate
446+
-> webpack:compile
447+
-> webpack:package
448+
449+
-> before:offline:start
450+
-> webpack:validate
451+
-> webpack:compile
452+
453+
-> before:offline:start:init
454+
-> webpack:validate
455+
-> webpack:compile
456+
```
457+
409458
## Release Notes
410459
411460
* 3.1.2

index.js

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ class ServerlessWebpack {
4848
webpack: {
4949
usage: 'Bundle with Webpack',
5050
lifecycleEvents: [
51-
'validate',
52-
'compile',
51+
'webpack'
5352
],
5453
options: {
5554
out: {
@@ -58,22 +57,23 @@ class ServerlessWebpack {
5857
},
5958
},
6059
commands: {
61-
invoke: {
62-
usage: 'Run a function locally from the webpack output bundle',
60+
validate: {
61+
type: 'entrypoint',
6362
lifecycleEvents: [
64-
'invoke',
63+
'validate',
6564
],
6665
},
67-
watch: {
68-
usage: 'Run a function from the webpack output bundle every time the source is changed',
66+
compile: {
67+
type: 'entrypoint',
6968
lifecycleEvents: [
70-
'watch',
69+
'compile',
7170
],
7271
},
73-
serve: {
74-
usage: 'Simulate the API Gateway and serves lambdas locally',
72+
package: {
73+
type: 'entrypoint',
7574
lifecycleEvents: [
76-
'serve',
75+
'packExternalModules',
76+
'packageModules'
7777
],
7878
},
7979
},
@@ -82,23 +82,21 @@ class ServerlessWebpack {
8282

8383
this.hooks = {
8484
'before:package:createDeploymentArtifacts': () => BbPromise.bind(this)
85-
.then(this.validate)
86-
.then(this.compile)
87-
.then(this.packExternalModules)
88-
.then(this.packageModules),
85+
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
86+
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
87+
.then(() => this.serverless.pluginManager.spawn('webpack:package')),
8988

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

9392
'before:deploy:function:packageFunction': () => BbPromise.bind(this)
94-
.then(this.validate)
95-
.then(this.compile)
96-
.then(this.packExternalModules)
97-
.then(this.packageModules),
93+
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
94+
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
95+
.then(() => this.serverless.pluginManager.spawn('webpack:package')),
9896

9997
'before:invoke:local:invoke': () => BbPromise.bind(this)
100-
.then(this.validate)
101-
.then(this.compile)
98+
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
99+
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
102100
.then(this.prepareLocalInvoke),
103101

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

112-
'webpack:validate': () => BbPromise.bind(this)
113-
.then(this.validate),
110+
'webpack:webpack': () => BbPromise.bind(this)
111+
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
112+
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
113+
.then(() => this.serverless.pluginManager.spawn('webpack:package')),
114114

115-
'webpack:compile': () => BbPromise.bind(this)
116-
.then(this.compile)
117-
.then(this.packExternalModules)
118-
.then(this.packageModules),
115+
/*
116+
* Internal webpack events (can be hooked by plugins)
117+
*/
118+
'webpack:validate:validate': () => BbPromise.bind(this)
119+
.then(this.validate),
119120

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

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

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

129130
'before:offline:start': () => BbPromise.bind(this)
130131
.then(this.prepareOfflineInvoke)
131-
.then(this.compile)
132+
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
132133
.then(this.wpwatch),
133134

134135
'before:offline:start:init': () => BbPromise.bind(this)
135136
.then(this.prepareOfflineInvoke)
136-
.then(this.compile)
137+
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
137138
.then(this.wpwatch),
138139

139140
};

lib/compile.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ module.exports = {
3737
});
3838

3939
this.compileOutputPaths = compileOutputPaths;
40+
this.compileStats = stats;
4041

41-
return stats;
42+
return BbPromise.resolve();
4243
});
4344
},
4445
};

lib/packExternalModules.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,17 @@ module.exports = {
158158
* This will utilize the npm cache at its best and give us the needed results
159159
* and performance.
160160
*/
161-
packExternalModules(stats) {
161+
packExternalModules() {
162+
163+
const stats = this.compileStats;
162164

163165
const includes = (
164166
this.serverless.service.custom &&
165167
this.serverless.service.custom.webpackIncludeModules
166168
);
167169

168170
if (!includes) {
169-
return BbPromise.resolve(stats);
171+
return BbPromise.resolve();
170172
}
171173

172174
// Read plugin configuration
@@ -231,7 +233,7 @@ module.exports = {
231233
if (_.isEmpty(compositeModules)) {
232234
// The compiled code does not reference any external modules at all
233235
this.serverless.cli.log('No external modules needed');
234-
return BbPromise.resolve(stats);
236+
return BbPromise.resolve();
235237
}
236238

237239
// (1.a) Install all needed modules
@@ -304,7 +306,7 @@ module.exports = {
304306
.tap(() => this.options.verbose && this.serverless.cli.log(`Prune: ${modulePath} [${_.now() - startPrune} ms]`));
305307
});
306308
})
307-
.return(stats);
309+
.return();
308310
});
309311
}
310312
};

lib/packageModules.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ function zip(directory, name) {
7777
}
7878

7979
module.exports = {
80-
packageModules(stats) {
80+
packageModules() {
81+
const stats = this.compileStats;
82+
8183
return BbPromise.mapSeries(stats.stats, (compileStats, index) => {
8284
const entryFunction = _.get(this.entryFunctions, index, {});
8385
const filename = `${entryFunction.funcName || this.serverless.service.getServiceObject().name}.zip`;

lib/prepareOfflineInvoke.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module.exports = {
1313
// Use service packaging for compile
1414
_.set(this.serverless, 'service.package.individually', false);
1515

16-
return this.validate()
16+
return this.serverless.pluginManager.spawn('webpack:validate')
1717
.then(() => {
1818
// Set offline location automatically if not set manually
1919
if (!this.options.location && !_.get(this.serverless, 'service.custom.serverless-offline.location')) {

tests/packExternalModules.test.js

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ describe('packExternalModules', () => {
154154

155155
it('should do nothing if webpackIncludeModules is not set', () => {
156156
_.unset(serverless, 'service.custom.webpackIncludeModules');
157-
return expect(module.packExternalModules({ stats: [] })).to.eventually.deep.equal({ stats: [] })
157+
module.compileStats = { stats: [] };
158+
return expect(module.packExternalModules()).to.be.fulfilled
158159
.then(() => BbPromise.all([
159160
expect(fsExtraMock.copy).to.not.have.been.called,
160161
expect(childProcessMock.exec).to.not.have.been.called,
@@ -188,7 +189,8 @@ describe('packExternalModules', () => {
188189
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
189190
childProcessMock.exec.onSecondCall().yields(null, '', '');
190191
childProcessMock.exec.onThirdCall().yields();
191-
return expect(module.packExternalModules(stats)).to.be.fulfilled
192+
module.compileStats = stats;
193+
return expect(module.packExternalModules()).to.be.fulfilled
192194
.then(() => BbPromise.all([
193195
// The module package JSON and the composite one should have been stored
194196
expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -217,7 +219,8 @@ describe('packExternalModules', () => {
217219
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
218220
childProcessMock.exec.onSecondCall().yields(new Error('npm install failed'));
219221
childProcessMock.exec.onThirdCall().yields();
220-
return expect(module.packExternalModules(stats)).to.be.rejectedWith('npm install failed')
222+
module.compileStats = stats;
223+
return expect(module.packExternalModules()).to.be.rejectedWith('npm install failed')
221224
.then(() => BbPromise.all([
222225
// npm ls and npm install should have been called
223226
expect(childProcessMock.exec).to.have.been.calledTwice,
@@ -230,7 +233,8 @@ describe('packExternalModules', () => {
230233
fsExtraMock.pathExists.yields(null, false);
231234
fsExtraMock.copy.yields();
232235
childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr);
233-
return expect(module.packExternalModules(stats)).to.be.rejectedWith('something went wrong')
236+
module.compileStats = stats;
237+
return expect(module.packExternalModules()).to.be.rejectedWith('something went wrong')
234238
.then(() => BbPromise.all([
235239
// The module package JSON and the composite one should have been stored
236240
expect(writeFileSyncStub).to.not.have.been.called,
@@ -250,7 +254,8 @@ describe('packExternalModules', () => {
250254
fsExtraMock.pathExists.yields(null, false);
251255
fsExtraMock.copy.yields();
252256
childProcessMock.exec.yields(new Error('something went wrong'), '{}', stderr);
253-
return expect(module.packExternalModules(stats)).to.be.rejectedWith('something went wrong')
257+
module.compileStats = stats;
258+
return expect(module.packExternalModules()).to.be.rejectedWith('something went wrong')
254259
.then(() => BbPromise.all([
255260
// The module package JSON and the composite one should have been stored
256261
expect(writeFileSyncStub).to.not.have.been.called,
@@ -311,7 +316,8 @@ describe('packExternalModules', () => {
311316
childProcessMock.exec.onFirstCall().yields(new Error('NPM error'), JSON.stringify(lsResult), stderr);
312317
childProcessMock.exec.onSecondCall().yields(null, '', '');
313318
childProcessMock.exec.onThirdCall().yields();
314-
return expect(module.packExternalModules(stats)).to.be.fulfilled
319+
module.compileStats = stats;
320+
return expect(module.packExternalModules()).to.be.fulfilled
315321
.then(() => BbPromise.all([
316322
// The module package JSON and the composite one should have been stored
317323
expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -338,9 +344,9 @@ describe('packExternalModules', () => {
338344
module.webpackOutputPath = 'outputPath';
339345
fsExtraMock.copy.yields();
340346
childProcessMock.exec.yields(null, '{}', '');
341-
return expect(module.packExternalModules(noExtStats)).to.be.fulfilled
342-
.then(stats => BbPromise.all([
343-
expect(stats).to.deep.equal(noExtStats),
347+
module.compileStats = noExtStats;
348+
return expect(module.packExternalModules()).to.be.fulfilled
349+
.then(() => BbPromise.all([
344350
// The module package JSON and the composite one should have been stored
345351
expect(writeFileSyncStub).to.not.have.been.called,
346352
// The modules should have been copied
@@ -382,7 +388,8 @@ describe('packExternalModules', () => {
382388
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
383389
childProcessMock.exec.onSecondCall().yields(null, '', '');
384390
childProcessMock.exec.onThirdCall().yields();
385-
return expect(module.packExternalModules(stats)).to.be.fulfilled
391+
module.compileStats = stats;
392+
return expect(module.packExternalModules()).to.be.fulfilled
386393
.then(() => BbPromise.all([
387394
// The module package JSON and the composite one should have been stored
388395
expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -436,7 +443,8 @@ describe('packExternalModules', () => {
436443
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
437444
childProcessMock.exec.onSecondCall().yields(null, '', '');
438445
childProcessMock.exec.onThirdCall().yields();
439-
return expect(module.packExternalModules(stats)).to.be.fulfilled
446+
module.compileStats = stats;
447+
return expect(module.packExternalModules()).to.be.fulfilled
440448
.then(() => BbPromise.all([
441449
// The module package JSON and the composite one should have been stored
442450
expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -489,7 +497,8 @@ describe('packExternalModules', () => {
489497
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
490498
childProcessMock.exec.onSecondCall().yields(null, '', '');
491499
childProcessMock.exec.onThirdCall().yields();
492-
return expect(module.packExternalModules(stats)).to.be.fulfilled
500+
module.compileStats = stats;
501+
return expect(module.packExternalModules()).to.be.fulfilled
493502
.then(() => BbPromise.all([
494503
// The module package JSON and the composite one should have been stored
495504
expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -537,7 +546,8 @@ describe('packExternalModules', () => {
537546
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
538547
childProcessMock.exec.onSecondCall().yields(null, '', '');
539548
childProcessMock.exec.onThirdCall().yields();
540-
return expect(module.packExternalModules(stats)).to.be.fulfilled
549+
module.compileStats = stats;
550+
return expect(module.packExternalModules()).to.be.fulfilled
541551
.then(() => BbPromise.all([
542552
// The module package JSON and the composite one should have been stored
543553
expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -589,7 +599,8 @@ describe('packExternalModules', () => {
589599
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
590600
childProcessMock.exec.onSecondCall().yields(null, '', '');
591601
childProcessMock.exec.onThirdCall().yields();
592-
return expect(module.packExternalModules(stats)).to.be.fulfilled
602+
module.compileStats = stats;
603+
return expect(module.packExternalModules()).to.be.fulfilled
593604
.then(() => BbPromise.all([
594605
// The module package JSON and the composite one should have been stored
595606
expect(writeFileSyncStub).to.have.been.calledTwice,
@@ -671,7 +682,8 @@ describe('packExternalModules', () => {
671682
childProcessMock.exec.onFirstCall().yields(null, JSON.stringify(dependencyGraph), '');
672683
childProcessMock.exec.onSecondCall().yields(null, '', '');
673684
childProcessMock.exec.onThirdCall().yields();
674-
return expect(module.packExternalModules(peerDepStats)).to.be.fulfilled
685+
module.compileStats = peerDepStats;
686+
return expect(module.packExternalModules()).to.be.fulfilled
675687
.then(() => BbPromise.all([
676688
// The module package JSON and the composite one should have been stored
677689
expect(writeFileSyncStub).to.have.been.calledTwice,

0 commit comments

Comments
 (0)