Skip to content

Commit c3d6abe

Browse files
authored
Merge pull request #270 from serverless-heaven/support-sls-run
Support SLS run
2 parents 34dba75 + a1f708c commit c3d6abe

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)