Skip to content

Commit

Permalink
Merge 167b094 into 71c86c3
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesmbourne committed Oct 16, 2019
2 parents 71c86c3 + 167b094 commit 7e37f16
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 42 deletions.
6 changes: 6 additions & 0 deletions README.md
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
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
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
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
6 changes: 4 additions & 2 deletions lib/compile.js
Expand Up @@ -36,11 +36,13 @@ 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;

// TODO: Mock & test this
this.saveCompileStats(stats);

return BbPromise.resolve();
});
Expand Down
46 changes: 46 additions & 0 deletions lib/compileStats.js
@@ -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
@@ -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
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
6 changes: 4 additions & 2 deletions lib/packageModules.js
Expand Up @@ -71,12 +71,14 @@ function zip(directory, name) {

module.exports = {
packageModules() {
const stats = this.compileStats;
// TODO: Test this is called
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
11 changes: 11 additions & 0 deletions tests/compile.test.js
Expand Up @@ -65,6 +65,10 @@ describe('compile', () => {
it('should compile with webpack from a context configuration', () => {
const testWebpackConfig = 'testconfig';
module.webpackConfig = testWebpackConfig;
module.saveCompileStats = function() {
return;
};

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 +98,10 @@ describe('compile', () => {
];
module.webpackConfig = testWebpackConfig;
module.multiCompile = true;
module.saveCompileStats = function() {
return;
};

webpackMock.compilerMock.run.reset();
webpackMock.compilerMock.run.yields(null, multiStats);
return expect(module.compile()).to.be.fulfilled.then(() => {
Expand All @@ -116,6 +124,9 @@ describe('compile', () => {
},
toString: sandbox.stub().returns('testStats')
};
module.saveCompileStats = function() {
return;
};

module.webpackConfig = testWebpackConfig;
webpackMock.compilerMock.run.reset();
Expand Down

0 comments on commit 7e37f16

Please sign in to comment.