Skip to content

Commit 0e68b54

Browse files
committed
Add initial implementation of --no-build
1 parent 71c86c3 commit 0e68b54

12 files changed

+262
-16
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,12 @@ Options are:
658658

659659
- `--out` or `-o` (optional) The output directory. Defaults to `.webpack`.
660660

661+
You may find this option useful in CI environments where you want to build the package once but deploy the same artifact to many environments. To use existing output, specify the `--no-build` flag.
662+
663+
```bash
664+
$ serverless deploy --no-build --out dist
665+
```
666+
661667
### Simulate API Gateway locally
662668

663669
:exclamation: The serve command has been removed. See above how to achieve the

index.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const prepareOfflineInvoke = require('./lib/prepareOfflineInvoke');
1414
const prepareStepOfflineInvoke = require('./lib/prepareStepOfflineInvoke');
1515
const packExternalModules = require('./lib/packExternalModules');
1616
const packageModules = require('./lib/packageModules');
17+
const compileStats = require('./lib/compileStats');
1718
const lib = require('./lib');
1819

1920
class ServerlessWebpack {
@@ -47,7 +48,8 @@ class ServerlessWebpack {
4748
prepareLocalInvoke,
4849
runPluginSupport,
4950
prepareOfflineInvoke,
50-
prepareStepOfflineInvoke
51+
prepareStepOfflineInvoke,
52+
compileStats
5153
);
5254

5355
this.commands = {
@@ -86,8 +88,15 @@ class ServerlessWebpack {
8688
this.hooks = {
8789
'before:package:createDeploymentArtifacts': () =>
8890
BbPromise.bind(this)
89-
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
90-
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
91+
.then(() => {
92+
// --no-build override
93+
if (this.options.build === false) {
94+
this.skipCompile = true;
95+
}
96+
97+
return this.serverless.pluginManager.spawn('webpack:validate');
98+
})
99+
.then(() => (this.skipCompile ? BbPromise.resolve() : this.serverless.pluginManager.spawn('webpack:compile')))
91100
.then(() => this.serverless.pluginManager.spawn('webpack:package')),
92101

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

index.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ describe('ServerlessWebpack', () => {
130130

131131
beforeEach(() => {
132132
ServerlessWebpack.lib.webpack.isLocal = false;
133+
slsw.options.build = true;
134+
slsw.skipCompile = false;
133135
});
134136

135137
after(() => {
@@ -154,6 +156,20 @@ describe('ServerlessWebpack', () => {
154156
return null;
155157
});
156158
});
159+
160+
it('should skip compile if requested', () => {
161+
slsw.options.build = false;
162+
return expect(slsw.hooks['before:package:createDeploymentArtifacts']()).to.be.fulfilled.then(() => {
163+
expect(slsw.serverless.pluginManager.spawn).to.have.been.calledTwice;
164+
expect(slsw.serverless.pluginManager.spawn.firstCall).to.have.been.calledWithExactly(
165+
'webpack:validate'
166+
);
167+
expect(slsw.serverless.pluginManager.spawn.secondCall).to.have.been.calledWithExactly(
168+
'webpack:package'
169+
);
170+
return null;
171+
});
172+
});
157173
}
158174
},
159175
{

lib/cleanup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module.exports = {
88
const webpackOutputPath = this.webpackOutputPath;
99

1010
const keepOutputDirectory = this.configuration.keepOutputDirectory;
11-
if (!keepOutputDirectory) {
11+
if (!keepOutputDirectory && !this.skipCompile) {
1212
this.options.verbose && this.serverless.cli.log(`Remove ${webpackOutputPath}`);
1313
if (this.serverless.utils.dirExistsSync(webpackOutputPath)) {
1414
fse.removeSync(webpackOutputPath);

lib/compile.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ module.exports = {
4040
});
4141

4242
this.compileOutputPaths = compileOutputPaths;
43-
this.compileStats = stats;
43+
44+
// TODO: Mock & test this
45+
this.compileStats.save(stats);
4446

4547
return BbPromise.resolve();
4648
});

lib/compileStats.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
const _ = require('lodash');
4+
5+
const statsFileName = 'stats.json';
6+
7+
function loadStatsFromFile(webpackOutputPath) {
8+
const statsFile = path.join(webpackOutputPath, statsFileName);
9+
const data = fs.readFileSync(statsFile);
10+
const stats = JSON.parse(data);
11+
12+
if (!stats.stats || !stats.stats.length) {
13+
throw new this.serverless.classes.Error('Packaging: No stats information found');
14+
}
15+
16+
const mappedStats = _.map(stats.stats, s =>
17+
_.assign({}, s, { outputPath: path.resolve(webpackOutputPath, s.outputPath) })
18+
);
19+
20+
return { stats: mappedStats };
21+
}
22+
23+
module.exports = {
24+
get() {
25+
const stats = this.stats || loadStatsFromFile.call(this, this.webpackOutputPath);
26+
27+
return stats;
28+
},
29+
save(stats) {
30+
this.stats = stats;
31+
32+
const statsJson = _.invokeMap(this.stats.stats, 'toJson');
33+
34+
const normalisedStats = _.map(statsJson, s => {
35+
return _.assign(s, { outputPath: path.relative(this.webpackOutputPath, s.outputPath) });
36+
});
37+
38+
fs.writeFileSync(path.join(this.webpackOutputPath, statsFileName), JSON.stringify(normalisedStats, null, 2));
39+
40+
return;
41+
}
42+
};

lib/compileStats.test.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use strict';
2+
3+
const _ = require('lodash');
4+
const BbPromise = require('bluebird');
5+
const chai = require('chai');
6+
const sinon = require('sinon');
7+
const path = require('path');
8+
const Serverless = require('serverless');
9+
10+
// Mocks
11+
const fsMockFactory = require('../tests/mocks/fs.mock');
12+
const mockery = require('mockery');
13+
14+
chai.use(require('chai-as-promised'));
15+
chai.use(require('sinon-chai'));
16+
17+
const expect = chai.expect;
18+
19+
describe('compileStats', () => {
20+
let baseModule;
21+
let module;
22+
let sandbox;
23+
let serverless;
24+
let fsMock;
25+
26+
before(() => {
27+
sandbox = sinon.createSandbox();
28+
sandbox.usingPromise(BbPromise.Promise);
29+
30+
fsMock = fsMockFactory.create(sandbox);
31+
32+
mockery.enable({ warnOnUnregistered: false });
33+
mockery.registerMock('fs', fsMock);
34+
35+
baseModule = require('./compileStats');
36+
Object.freeze(baseModule);
37+
});
38+
39+
beforeEach(() => {
40+
serverless = new Serverless();
41+
serverless.cli = {
42+
log: sandbox.stub()
43+
};
44+
module = _.assign(
45+
{
46+
serverless,
47+
options: {}
48+
},
49+
baseModule
50+
);
51+
});
52+
53+
afterEach(() => {
54+
mockery.disable();
55+
mockery.deregisterAll();
56+
sandbox.restore();
57+
});
58+
59+
describe('get', () => {
60+
it('should return this.stats if available', () => {
61+
const stats = { stats: [{}] };
62+
module.stats = stats;
63+
64+
const result = module.get();
65+
66+
expect(result).to.equal(stats);
67+
});
68+
69+
it('should load stats from file if this.stats is not present', () => {
70+
const webpackOutputPath = '.webpack';
71+
72+
const statsFile = { stats: [{ outputPath: 'service/path' }] };
73+
const mappedFile = { stats: [{ outputPath: path.resolve(webpackOutputPath, 'service/path') }] };
74+
module.webpackOutputPath = webpackOutputPath;
75+
76+
const fullStatsPath = `${webpackOutputPath}/stats.json`;
77+
78+
fsMock.readFileSync.withArgs(fullStatsPath).returns(JSON.stringify(statsFile));
79+
80+
const stats = module.get();
81+
82+
expect(fsMock.readFileSync).to.be.calledWith(fullStatsPath);
83+
expect(stats).to.deep.equal(mappedFile);
84+
});
85+
86+
it('should fail if compile stats are not loaded', () => {
87+
const webpackOutputPath = '.webpack';
88+
89+
const statsFile = { stats: [] };
90+
91+
module.webpackOutputPath = webpackOutputPath;
92+
93+
const fullStatsPath = `${webpackOutputPath}/stats.json`;
94+
95+
fsMock.readFileSync.withArgs(fullStatsPath).returns(JSON.stringify(statsFile));
96+
97+
expect(() => module.get()).to.throw(/Packaging: No stats information found/);
98+
});
99+
});
100+
101+
describe('save', () => {
102+
it('should set this.stats', () => {
103+
// TODO:
104+
return;
105+
});
106+
});
107+
});

lib/packExternalModules.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,18 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl
127127
if (!_.includes(ignoredDevDependencies, module.external)) {
128128
// Runtime dependency found in devDependencies but not forcefully excluded
129129
this.serverless.cli.log(
130-
`ERROR: Runtime dependency '${module.external}' found in devDependencies. Move it to dependencies or use forceExclude to explicitly exclude it.`
130+
`ERROR: Runtime dependency '${
131+
module.external
132+
}' found in devDependencies. Move it to dependencies or use forceExclude to explicitly exclude it.`
131133
);
132134
throw new this.serverless.classes.Error(`Serverless-webpack dependency error: ${module.external}.`);
133135
}
134136

135137
this.options.verbose &&
136138
this.serverless.cli.log(
137-
`INFO: Runtime dependency '${module.external}' found in devDependencies. It has been excluded automatically.`
139+
`INFO: Runtime dependency '${
140+
module.external
141+
}' found in devDependencies. It has been excluded automatically.`
138142
);
139143
}
140144
}

lib/packageModules.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ function zip(directory, name) {
7171

7272
module.exports = {
7373
packageModules() {
74-
const stats = this.compileStats;
74+
// TODO: Test this is called
75+
const stats = this.compileStats.get();
7576

7677
return BbPromise.mapSeries(stats.stats, (compileStats, index) => {
7778
const entryFunction = _.get(this.entryFunctions, index, {});

lib/validate.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ module.exports = {
3535
if (_.isEmpty(files)) {
3636
// If we cannot find any handler we should terminate with an error
3737
throw new this.serverless.classes.Error(
38-
`No matching handler found for '${fileName}' in '${this.serverless.config.servicePath}'. Check your service definition.`
38+
`No matching handler found for '${fileName}' in '${
39+
this.serverless.config.servicePath
40+
}'. Check your service definition.`
3941
);
4042
}
4143

@@ -222,6 +224,18 @@ module.exports = {
222224
this.webpackConfig.output.path = path.join(this.webpackConfig.output.path, 'service');
223225
}
224226

227+
// Skip compile phase
228+
if (this.options.build === false) {
229+
if (_.get(this.serverless, 'service.package.individually')) {
230+
const err = new this.serverless.classes.Error(
231+
'Usage of --no-build is not currently supported with individual packaging'
232+
);
233+
BbPromise.reject(err);
234+
}
235+
236+
this.skipCompile = true;
237+
}
238+
225239
return BbPromise.resolve();
226240
};
227241

0 commit comments

Comments
 (0)