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

Skip compile & packaging if --no-build is set #504

Closed
wants to merge 10 commits into from
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,12 @@ Options are:

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

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.

```bash
$ serverless deploy --no-build --out dist
```

### Simulate API Gateway locally

:exclamation: The serve command has been removed. See above how to achieve the
Expand Down
15 changes: 12 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const prepareOfflineInvoke = require('./lib/prepareOfflineInvoke');
const prepareStepOfflineInvoke = require('./lib/prepareStepOfflineInvoke');
const packExternalModules = require('./lib/packExternalModules');
const packageModules = require('./lib/packageModules');
const compileStats = require('./lib/compileStats');
const lib = require('./lib');

class ServerlessWebpack {
Expand Down Expand Up @@ -47,7 +48,8 @@ class ServerlessWebpack {
prepareLocalInvoke,
runPluginSupport,
prepareOfflineInvoke,
prepareStepOfflineInvoke
prepareStepOfflineInvoke,
compileStats
);

this.commands = {
Expand Down Expand Up @@ -86,8 +88,15 @@ class ServerlessWebpack {
this.hooks = {
'before:package:createDeploymentArtifacts': () =>
BbPromise.bind(this)
.then(() => this.serverless.pluginManager.spawn('webpack:validate'))
.then(() => this.serverless.pluginManager.spawn('webpack:compile'))
.then(() => {
// --no-build override
if (this.options.build === false) {
this.skipCompile = true;
}

return this.serverless.pluginManager.spawn('webpack:validate');
})
.then(() => (this.skipCompile ? BbPromise.resolve() : this.serverless.pluginManager.spawn('webpack:compile')))
.then(() => this.serverless.pluginManager.spawn('webpack:package')),

'after:package:createDeploymentArtifacts': () => BbPromise.bind(this).then(this.cleanup),
Expand Down
16 changes: 16 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ describe('ServerlessWebpack', () => {

beforeEach(() => {
ServerlessWebpack.lib.webpack.isLocal = false;
slsw.options.build = true;
slsw.skipCompile = false;
});

after(() => {
Expand All @@ -154,6 +156,20 @@ describe('ServerlessWebpack', () => {
return null;
});
});

it('should skip compile if requested', () => {
slsw.options.build = false;
return expect(slsw.hooks['before:package:createDeploymentArtifacts']()).to.be.fulfilled.then(() => {
expect(slsw.serverless.pluginManager.spawn).to.have.been.calledTwice;
expect(slsw.serverless.pluginManager.spawn.firstCall).to.have.been.calledWithExactly(
'webpack:validate'
);
expect(slsw.serverless.pluginManager.spawn.secondCall).to.have.been.calledWithExactly(
'webpack:package'
);
return null;
});
});
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion lib/cleanup.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {
const webpackOutputPath = this.webpackOutputPath;

const keepOutputDirectory = this.configuration.keepOutputDirectory;
if (!keepOutputDirectory) {
if (!keepOutputDirectory && !this.skipCompile) {
this.options.verbose && this.serverless.cli.log(`Remove ${webpackOutputPath}`);
if (this.serverless.utils.dirExistsSync(webpackOutputPath)) {
fse.removeSync(webpackOutputPath);
Expand Down
5 changes: 3 additions & 2 deletions lib/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ module.exports = {
throw new Error('Webpack compilation error, see above');
}

compileOutputPaths.push(compileStats.compilation.compiler.outputPath);
compileOutputPaths.push(compileStats.outputPath);
});

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

this.saveCompileStats(stats);

return BbPromise.resolve();
});
Expand Down
46 changes: 46 additions & 0 deletions lib/compileStats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const path = require('path');
const fs = require('fs');
const _ = require('lodash');

const statsFileName = 'stats.json';

function loadStatsFromFile(webpackOutputPath) {
const statsFile = getStatsFilePath(webpackOutputPath);
const data = fs.readFileSync(statsFile);
const stats = JSON.parse(data);

if (!stats.stats || !stats.stats.length) {
throw new this.serverless.classes.Error('Packaging: No stats information found');
}

const mappedStats = _.map(stats.stats, s =>
_.assign({}, s, { outputPath: path.resolve(webpackOutputPath, s.outputPath) })
);

return { stats: mappedStats };
}

const getStatsFilePath = webpackOutputPath => path.join(webpackOutputPath, statsFileName);

module.exports = {
getCompileStats() {
const stats = this.stats || loadStatsFromFile.call(this, this.webpackOutputPath);

return stats;
},
saveCompileStats(stats) {
const statsJson = _.invokeMap(stats.stats, 'toJson');

this.stats = { stats: statsJson };

const normalisedStats = _.map(statsJson, s => {
return _.assign({}, s, { outputPath: path.relative(this.webpackOutputPath, s.outputPath) });
});

const statsFile = getStatsFilePath(this.webpackOutputPath);

fs.writeFileSync(statsFile, JSON.stringify({ stats: normalisedStats }, null, 2));

return;
}
};
130 changes: 130 additions & 0 deletions lib/compileStats.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use strict';

const _ = require('lodash');
const BbPromise = require('bluebird');
const chai = require('chai');
const sinon = require('sinon');
const path = require('path');
const Serverless = require('serverless');

// Mocks
const fsMockFactory = require('../tests/mocks/fs.mock');
const mockery = require('mockery');

chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));

const expect = chai.expect;

describe('compileStats', () => {
let baseModule;
let module;
let sandbox;
let serverless;
let fsMock;

before(() => {
sandbox = sinon.createSandbox();
sandbox.usingPromise(BbPromise.Promise);

fsMock = fsMockFactory.create(sandbox);

mockery.enable({ warnOnUnregistered: false });
mockery.registerMock('fs', fsMock);

baseModule = require('./compileStats');
Object.freeze(baseModule);
});

beforeEach(() => {
serverless = new Serverless();
serverless.cli = {
log: sandbox.stub()
};
module = _.assign(
{
serverless,
options: {}
},
baseModule
);
});

afterEach(() => {
fsMock.writeFileSync.reset();
fsMock.readFileSync.reset();
mockery.disable();
mockery.deregisterAll();
sandbox.restore();
});

describe('getCompileStats', () => {
it('should return this.stats if available', () => {
const stats = { stats: [{}] };
module.stats = stats;

const result = module.getCompileStats();

expect(result).to.equal(stats);
});

it('should load stats from file if this.stats is not present', () => {
const webpackOutputPath = '.webpack';

const statsFile = { stats: [{ outputPath: 'service/path' }] };
const mappedFile = { stats: [{ outputPath: path.resolve(webpackOutputPath, 'service', 'path') }] };
module.webpackOutputPath = webpackOutputPath;

const fullStatsPath = path.join(webpackOutputPath, 'stats.json');

fsMock.readFileSync.withArgs(fullStatsPath).returns(JSON.stringify(statsFile));

const stats = module.getCompileStats();

expect(fsMock.readFileSync).to.be.calledWith(fullStatsPath);
expect(stats).to.deep.equal(mappedFile);
});

it('should fail if compile stats are not loaded', () => {
const webpackOutputPath = '.webpack';

const statsFile = { stats: [] };

module.webpackOutputPath = webpackOutputPath;

const fullStatsPath = path.join(webpackOutputPath, 'stats.json');

fsMock.readFileSync.withArgs(fullStatsPath).returns(JSON.stringify(statsFile));

expect(() => module.getCompileStats()).to.throw(/Packaging: No stats information found/);
});
});

describe('saveCompileStats', () => {
it('should set this.stats', () => {
const webpackOutputPath = '.webpack';
module.webpackOutputPath = webpackOutputPath;

const stats = { stats: [{ toJson: () => ({ outputPath: '.webpack/service/path' }) }] };

module.saveCompileStats(stats);

expect(module.stats).to.deep.equal({ stats: [{ outputPath: '.webpack/service/path' }] });
});

it('should write stats to a file', () => {
const webpackOutputPath = '/tmp/.webpack';
module.webpackOutputPath = webpackOutputPath;

const stats = { stats: [{ toJson: () => ({ outputPath: '/tmp/.webpack/service/path' }) }] };

const fullStatsPath = path.join(webpackOutputPath, 'stats.json');

const fileContent = JSON.stringify({ stats: [{ outputPath: path.join('service', 'path') }] }, null, 2);

module.saveCompileStats(stats);

expect(fsMock.writeFileSync).to.be.calledWith(fullStatsPath, fileContent);
});
});
});
8 changes: 6 additions & 2 deletions lib/packExternalModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,18 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl
if (!_.includes(ignoredDevDependencies, module.external)) {
// Runtime dependency found in devDependencies but not forcefully excluded
this.serverless.cli.log(
`ERROR: Runtime dependency '${module.external}' found in devDependencies. Move it to dependencies or use forceExclude to explicitly exclude it.`
`ERROR: Runtime dependency '${
module.external
}' found in devDependencies. Move it to dependencies or use forceExclude to explicitly exclude it.`
);
throw new this.serverless.classes.Error(`Serverless-webpack dependency error: ${module.external}.`);
}

this.options.verbose &&
this.serverless.cli.log(
`INFO: Runtime dependency '${module.external}' found in devDependencies. It has been excluded automatically.`
`INFO: Runtime dependency '${
module.external
}' found in devDependencies. It has been excluded automatically.`
);
}
}
Expand Down
5 changes: 3 additions & 2 deletions lib/packageModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,13 @@ function zip(directory, name) {

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

return BbPromise.mapSeries(stats.stats, (compileStats, index) => {
const entryFunction = _.get(this.entryFunctions, index, {});
const filename = `${entryFunction.funcName || this.serverless.service.getServiceObject().name}.zip`;
const modulePath = compileStats.compilation.compiler.outputPath;

const modulePath = compileStats.outputPath;

const startZip = _.now();
return zip
Expand Down
8 changes: 8 additions & 0 deletions tests/compile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ describe('compile', () => {
it('should compile with webpack from a context configuration', () => {
const testWebpackConfig = 'testconfig';
module.webpackConfig = testWebpackConfig;
module.saveCompileStats = sandbox.stub();

return expect(module.compile()).to.be.fulfilled.then(() => {
expect(webpackMock).to.have.been.calledWith(testWebpackConfig);
expect(webpackMock.compilerMock.run).to.have.been.calledOnce;
Expand Down Expand Up @@ -94,6 +96,8 @@ describe('compile', () => {
];
module.webpackConfig = testWebpackConfig;
module.multiCompile = true;
module.saveCompileStats = sandbox.stub();

webpackMock.compilerMock.run.reset();
webpackMock.compilerMock.run.yields(null, multiStats);
return expect(module.compile()).to.be.fulfilled.then(() => {
Expand All @@ -117,13 +121,17 @@ describe('compile', () => {
toString: sandbox.stub().returns('testStats')
};

const mockSaveCompileStats = sandbox.stub();
module.saveCompileStats = mockSaveCompileStats;

module.webpackConfig = testWebpackConfig;
webpackMock.compilerMock.run.reset();
webpackMock.compilerMock.run.yields(null, mockStats);
return expect(module.compile())
.to.be.fulfilled.then(() => {
expect(webpackMock).to.have.been.calledWith(testWebpackConfig);
expect(mockStats.toString.firstCall.args).to.eql([testWebpackConfig.stats]);
expect(mockSaveCompileStats).to.have.been.calledWith({ stats: [mockStats] });
module.webpackConfig = [testWebpackConfig];
return expect(module.compile()).to.be.fulfilled;
})
Expand Down
Loading