Skip to content

Commit a1f708c

Browse files
author
Frank Schmid
committed
Support serverless run
Small README change Raise coverage again Added unit tests Added serverless run to README Added watch unit tests with handler function. Support --watch for run command Package external modules for run Hook run:run, set service packaging and change to compiled directory
1 parent 34dba75 commit a1f708c

File tree

9 files changed

+269
-7
lines changed

9 files changed

+269
-7
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and much more!
1818
* Configuration possibilities range from zero-config to fully customizable
1919
* Support of `serverless package`, `serverless deploy` and `serverless deploy function`
2020
* Support of `serverless invoke local` and `serverless invoke local --watch`
21+
* Support of `serverless run` and `serverless run --watch`
2122
* Integrates with [`serverless-offline`][link-serverless-offline] to simulate local API Gateway endpoints
2223
* When enabled in your service configuration, functions are packaged and compiled
2324
individually, resulting in smaller Lambda packages that contain only the code and
@@ -339,6 +340,19 @@ All options that are supported by invoke local can be used as usual:
339340

340341
> :exclamation: The old `webpack watch` command has been disabled.
341342

343+
### Usage with serverless run (Serverless Event Gateway)
344+
345+
The `serverless run` command is supported with the plugin. To test a local
346+
service with the Serverless Emulator, you can use the `serverless run`
347+
command as documented by Serverless. The command will compile the code before
348+
it uploads it into the event gateway.
349+
350+
#### Serverless run with webpack watch mode
351+
352+
You can enable source watch mode with `serverless run --watch`. The plugin will
353+
then watch for any source changes, recompile and redeploy the code to the event
354+
gateway. So you can just keep the event gateway running and test new code immediately.
355+
342356
### Usage with serverless-offline
343357

344358
The plugin integrates very well with [serverless-offline][link-serverless-offline] to

index.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
const BbPromise = require('bluebird');
44
const _ = require('lodash');
5+
const path = require('path');
56

67
const validate = require('./lib/validate');
78
const compile = require('./lib/compile');
89
const wpwatch = require('./lib/wpwatch');
910
const cleanup = require('./lib/cleanup');
1011
const run = require('./lib/run');
1112
const prepareLocalInvoke = require('./lib/prepareLocalInvoke');
13+
const runPluginSupport = require('./lib/runPluginSupport');
1214
const prepareOfflineInvoke = require('./lib/prepareOfflineInvoke');
1315
const packExternalModules = require('./lib/packExternalModules');
1416
const packageModules = require('./lib/packageModules');
@@ -41,6 +43,7 @@ class ServerlessWebpack {
4143
packExternalModules,
4244
packageModules,
4345
prepareLocalInvoke,
46+
runPluginSupport,
4447
prepareOfflineInvoke
4548
);
4649

@@ -102,7 +105,22 @@ class ServerlessWebpack {
102105
'after:invoke:local:invoke': () => BbPromise.bind(this)
103106
.then(() => {
104107
if (this.options.watch && !this.isWatching) {
105-
return this.watch();
108+
return this.watch('invoke:local');
109+
}
110+
return BbPromise.resolve();
111+
}),
112+
113+
'before:run:run': () => BbPromise.bind(this)
114+
.then(() => _.set(this.serverless, 'service.package.individually', false))
115+
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
116+
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
117+
.then(this.packExternalModules)
118+
.then(this.prepareRun),
119+
120+
'after:run:run': () => BbPromise.bind(this)
121+
.then(() => {
122+
if (this.options.watch && !this.isWatching) {
123+
return this.watch(this.watchRun.bind(this));
106124
}
107125
return BbPromise.resolve();
108126
}),

lib/run.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ const BbPromise = require('bluebird');
55
const webpack = require('webpack');
66

77
module.exports = {
8-
watch() {
8+
watch(command) {
99
const functionName = this.options.function;
10-
this.serverless.cli.log(`Watch function ${functionName}...`);
10+
if (functionName) {
11+
this.serverless.cli.log(`Watch function ${functionName}...`);
12+
} else {
13+
this.serverless.cli.log('Watch service...');
14+
}
1115

1216
const compiler = webpack(this.webpackConfig);
1317
const watchOptions = {};
@@ -34,7 +38,11 @@ module.exports = {
3438
}
3539

3640
this.serverless.cli.log('Sources changed.');
37-
return this.serverless.pluginManager.spawn('invoke:local');
41+
if (_.isFunction(command)) {
42+
return command();
43+
}
44+
this.options.verbose && this.serverless.cli.log(`Invoke ${command}`);
45+
return this.serverless.pluginManager.spawn(command);
3846
})
3947
.then(() => this.serverless.cli.log('Waiting for changes ...'));
4048
});

lib/runPluginSupport.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
const BbPromise = require('bluebird');
4+
const path = require('path');
5+
6+
module.exports = {
7+
prepareRun() {
8+
this.originalServicePath = this.serverless.config.servicePath;
9+
this.originalWebpackOutputPath = this.webpackOutputPath;
10+
11+
this.serverless.config.servicePath = path.join(this.webpackOutputPath, 'service');
12+
13+
// Set service path as CWD to allow accessing bundled files correctly
14+
process.chdir(this.serverless.config.servicePath);
15+
16+
// Prevent a respawn to delete our output directory
17+
this.keepOutputDirectory = true;
18+
19+
return BbPromise.resolve();
20+
},
21+
22+
watchRun() {
23+
// Redeploy functions to the event gateway
24+
// We have to use the internal functions here, because the run plugin
25+
// does not offer any detailed hooks that could be overridden to do
26+
// a deploy only. Running the whole run command will lead to an error
27+
// because the functions are already registered in the event gateway
28+
const deployFunctionsToLocalEmulator = require(
29+
path.join(
30+
this.serverless.config.serverlessPath,
31+
'plugins',
32+
'run',
33+
'utils',
34+
'deployFunctionsToLocalEmulator'
35+
)
36+
);
37+
const getLocalRootUrl = require(
38+
path.join(
39+
this.serverless.config.serverlessPath,
40+
'plugins',
41+
'run',
42+
'utils',
43+
'getLocalRootUrl'
44+
)
45+
);
46+
47+
// Reset configuration
48+
this.serverless.config.servicePath = this.originalServicePath;
49+
this.webpackOutputPath = this.originalWebpackOutputPath;
50+
this.webpackConfig.output.path = this.webpackOutputPath;
51+
process.chdir(this.serverless.config.servicePath);
52+
53+
return this.hooks['before:run:run']()
54+
.then(() => deployFunctionsToLocalEmulator(
55+
this.serverless.service,
56+
this.serverless.config.servicePath,
57+
getLocalRootUrl(this.options.lport)
58+
));
59+
}
60+
};

lib/validate.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ module.exports = {
139139
this.webpackConfig.output.path = path.join(this.serverless.config.servicePath, this.options.out);
140140
}
141141

142-
fse.removeSync(this.webpackConfig.output.path);
142+
if (!this.keepOutputDirectory) {
143+
this.options.verbose && this.serverless.cli.log(`Removing ${this.webpackConfig.output.path}`);
144+
fse.removeSync(this.webpackConfig.output.path);
145+
}
143146
this.webpackOutputPath = this.webpackConfig.output.path;
144147

145148
// In case of individual packaging we have to create a separate config for each function

tests/all.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ describe('serverless-webpack', () => {
88
require('./run.test');
99
require('./cleanup.test');
1010
require('./wpwatch.test');
11+
require('./runPluginSupport.test');
1112
});

tests/run.test.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ describe('run', () => {
8080
module.isWatching = false;
8181
const watch = module.watch.bind(module);
8282
webpackMock.compilerMock.watch = sandbox.stub().yields(null, {});
83+
_.set(module, 'options.function', 'myFunction');
8384

84-
watch();
85+
watch('invoke:local');
8586
expect(spawnStub).to.not.have.been.called;
8687
expect(module.isWatching).to.be.true;
8788
});
@@ -91,12 +92,36 @@ describe('run', () => {
9192
const watch = module.watch.bind(module);
9293
webpackMock.compilerMock.watch = sandbox.stub().yields(null, {});
9394

94-
watch();
95+
watch('invoke:local');
9596
expect(spawnStub).to.have.been.calledOnce;
9697
expect(spawnStub).to.have.been.calledWith('invoke:local');
9798
expect(module.isWatching).to.be.true;
9899
});
99100

101+
it('should not call given handler function on first run', () => {
102+
module.isWatching = false;
103+
const watch = module.watch.bind(module);
104+
const watchHandler = sandbox.stub().returns(BbPromise.resolve());
105+
webpackMock.compilerMock.watch = sandbox.stub().yields(null, {});
106+
107+
watch(watchHandler);
108+
expect(spawnStub).to.not.have.been.called;
109+
expect(watchHandler).to.not.have.been.called;
110+
expect(module.isWatching).to.be.true;
111+
});
112+
113+
it('should call given handler function on subsequent runs', () => {
114+
module.isWatching = true;
115+
const watch = module.watch.bind(module);
116+
const watchHandler = sandbox.stub().returns(BbPromise.resolve());
117+
webpackMock.compilerMock.watch = sandbox.stub().yields(null, {});
118+
119+
watch(watchHandler);
120+
expect(spawnStub).to.have.not.been.called;
121+
expect(watchHandler).to.have.been.calledOnce;
122+
expect(module.isWatching).to.be.true;
123+
});
124+
100125
it('should reset the service path', () => {
101126
module.isWatching = true;
102127
module.originalServicePath = 'originalPath';

tests/runPluginSupport.test.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use strict';
2+
3+
const BbPromise = require('bluebird');
4+
const _ = require('lodash');
5+
const chai = require('chai');
6+
const sinon = require('sinon');
7+
const mockery = require('mockery');
8+
const Serverless = require('serverless');
9+
const path = require('path');
10+
11+
chai.use(require('chai-as-promised'));
12+
chai.use(require('sinon-chai'));
13+
14+
const expect = chai.expect;
15+
16+
describe('runPluginSupport', () => {
17+
let sandbox;
18+
let baseModule;
19+
let serverless;
20+
let module;
21+
let chdirStub;
22+
let getLocalRootUrlStub;
23+
let deployFunctionsToLocalEmulatorStub;
24+
25+
before(() => {
26+
sandbox = sinon.createSandbox();
27+
sandbox.usingPromise(BbPromise.Promise);
28+
29+
const pluginRunUtils = path.join(
30+
'.',
31+
'plugins',
32+
'run',
33+
'utils'
34+
);
35+
36+
deployFunctionsToLocalEmulatorStub = sandbox.stub().resolves();
37+
getLocalRootUrlStub = sandbox.stub();
38+
39+
mockery.enable({ warnOnUnregistered: false });
40+
mockery.registerMock(path.join(pluginRunUtils, 'deployFunctionsToLocalEmulator'), deployFunctionsToLocalEmulatorStub);
41+
mockery.registerMock(path.join(pluginRunUtils, 'getLocalRootUrl'), getLocalRootUrlStub);
42+
baseModule = require('../lib/runPluginSupport');
43+
Object.freeze(baseModule);
44+
});
45+
46+
after(() => {
47+
mockery.disable();
48+
mockery.deregisterAll();
49+
});
50+
51+
beforeEach(() => {
52+
serverless = new Serverless();
53+
serverless.cli = {
54+
log: sandbox.stub(),
55+
consoleLog: sandbox.stub()
56+
};
57+
58+
module = _.assign({
59+
serverless,
60+
options: {},
61+
}, baseModule);
62+
63+
_.set(serverless, 'config.serverlessPath', '.');
64+
65+
chdirStub = sandbox.stub(process, 'chdir');
66+
});
67+
68+
afterEach(() => {
69+
chdirStub.reset();
70+
sandbox.restore();
71+
});
72+
73+
describe('prepareRun', () => {
74+
it('should prepare environment and save original values', () => {
75+
const prepareRun = module.prepareRun.bind(module);
76+
const servicePath = path.join('my', 'servicePath');
77+
const webpackOutputPath = path.join('webpack', 'output', 'path');
78+
79+
_.set(serverless, 'config.servicePath', servicePath);
80+
_.set(module, 'webpackOutputPath', webpackOutputPath);
81+
_.unset(module, 'keepOutputDirectory');
82+
83+
return expect(prepareRun()).to.be.fulfilled
84+
.then(() => BbPromise.join(
85+
expect(module.originalServicePath).to.equal(servicePath),
86+
expect(module.originalWebpackOutputPath).to.equal(webpackOutputPath),
87+
expect(module.keepOutputDirectory).to.be.true,
88+
expect(serverless.config.servicePath).to.equal(path.join(webpackOutputPath, 'service')),
89+
expect(chdirStub).to.have.been.calledWith(serverless.config.servicePath)
90+
));
91+
});
92+
});
93+
94+
describe('watchRun', () => {
95+
beforeEach(() => {
96+
_.set(module, 'webpackConfig.output.path', 'outputPath');
97+
});
98+
99+
it('should invoke hook and deploy functions', () => {
100+
const watchRun = module.watchRun.bind(module);
101+
const service = {
102+
name: 'testService',
103+
functions: {}
104+
};
105+
_.set(module, 'hooks[before:run:run]', sandbox.stub().resolves());
106+
_.set(serverless, 'service', service);
107+
108+
return expect(watchRun()).to.be.fulfilled
109+
.then(() => BbPromise.join(
110+
expect(deployFunctionsToLocalEmulatorStub).to.have.been.calledOnce,
111+
expect(getLocalRootUrlStub).to.have.been.calledOnce,
112+
expect(deployFunctionsToLocalEmulatorStub).to.have.been.calledWith(service)
113+
));
114+
});
115+
});
116+
});

tests/validate.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe('validate', () => {
5151
});
5252

5353
afterEach(() => {
54+
fsExtraMock.removeSync.reset();
5455
sandbox.restore();
5556
});
5657

@@ -87,6 +88,22 @@ describe('validate', () => {
8788
.then(() => expect(fsExtraMock.removeSync).to.have.been.calledWith(testOutPath));
8889
});
8990

91+
it('should keep the output path if requested', () => {
92+
const testOutPath = 'test';
93+
const testConfig = {
94+
entry: 'test',
95+
context: 'testcontext',
96+
output: {
97+
path: testOutPath,
98+
},
99+
};
100+
_.set(module, 'keepOutputDirectory', true);
101+
module.serverless.service.custom.webpack = testConfig;
102+
return module
103+
.validate()
104+
.then(() => expect(fsExtraMock.removeSync).to.not.have.been.called);
105+
});
106+
90107
it('should override the output path if `out` option is specified', () => {
91108
const testConfig = {
92109
entry: 'test',

0 commit comments

Comments
 (0)