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

#315 - Add watch-compile event #319

Merged
merged 7 commits into from Feb 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -467,6 +467,7 @@ All events (H) can be hooked by a plugin.
-> webpack:validate:validate (H)
-> webpack:compile
-> webpack:compile:compile (H)
-> webpack:compile:watch:compile (H)
-> webpack:package
-> webpack:package:packExternalModules (H)
-> webpack:package:packageModules (H)
Expand Down
10 changes: 10 additions & 0 deletions index.js
Expand Up @@ -71,6 +71,14 @@ class ServerlessWebpack {
lifecycleEvents: [
'compile',
],
commands: {
watch: {
type: 'entrypoint',
lifecycleEvents: [
'compile'
]
}
}
},
package: {
type: 'entrypoint',
Expand Down Expand Up @@ -139,6 +147,8 @@ class ServerlessWebpack {
'webpack:compile:compile': () => BbPromise.bind(this)
.then(this.compile),

'webpack:compile:watch:compile': () => BbPromise.resolve(),

'webpack:package:packExternalModules': () => BbPromise.bind(this)
.then(this.packExternalModules),

Expand Down
32 changes: 29 additions & 3 deletions lib/wpwatch.js
Expand Up @@ -31,9 +31,10 @@ module.exports = {
};

// This starts the watch and waits for the immediate compile that follows to end or fail.
let lastHash = null;
const startWatch = (callback) => {
let firstRun = true;
compiler.watch(watchOptions, (err, stats) => {
const watcher = compiler.watch(watchOptions, (err, stats) => {
if (err) {
if (firstRun) {
firstRun = false;
Expand All @@ -42,15 +43,40 @@ module.exports = {
throw err;
}

process.env.SLS_DEBUG && this.serverless.cli.log(`Webpack watch invoke: HASH NEW=${stats.hash} CUR=${lastHash}`);

// If the file hash did not change there were no effective code changes detected
// (comment changes do not change the compile hash and do not account for a rebuild!)
// See here: https://webpack.js.org/api/node/#watching (note below watching)
if (stats && stats.hash === lastHash) {
if (firstRun) {
firstRun = false;
callback();
}
return;
}

if (stats) {
lastHash = stats.hash;
this.serverless.cli.consoleLog(stats.toString(consoleStats));
}

this.serverless.cli.log('Watching for changes...');

if (firstRun) {
firstRun = false;
this.serverless.cli.log('Watching for changes...');
callback();
} else {
// We will close the watcher while the compile event is triggered and resume afterwards to prevent race conditions.
watcher.close(() => {
return this.serverless.pluginManager.spawn('webpack:compile:watch')
.then(() => {
// Resume watching after we triggered the compile:watch event
return BbPromise.fromCallback(cb => {
startWatch(cb);
})
.then(() => this.serverless.cli.log('Watching for changes...'));
});
});
}
});
};
Expand Down
23 changes: 23 additions & 0 deletions tests/run.test.js
Expand Up @@ -87,6 +87,29 @@ describe('run', () => {
expect(module.isWatching).to.be.true;
});

it('should not spawn on watch first run', () => {
module.isWatching = false;
const watch = module.watch.bind(module);
webpackMock.compilerMock.watch = sandbox.stub().yields(null, {});
_.set(module, 'options.function', 'myFunction');

watch('compile:watch:compile');
expect(spawnStub).to.not.have.been.called;
expect(module.isWatching).to.be.true;
});

it('should spawn on watch second run', () => {
module.isWatching = false;
const watch = module.watch.bind(module);
webpackMock.compilerMock.watch = sandbox.stub().yields(null, {});
_.set(module, 'options.function', 'myFunction');

watch('compile:watch:compile');
watch('compile:watch:compile');
expect(spawnStub).to.have.been.calledOnce;
expect(module.isWatching).to.be.true;
});

it('should spawn invoke local on subsequent runs', () => {
module.isWatching = true;
const watch = module.watch.bind(module);
Expand Down
12 changes: 9 additions & 3 deletions tests/webpack.mock.js
Expand Up @@ -12,17 +12,23 @@ const StatsMock = () => ({
toString: sinon.stub().returns('testStats'),
});

const CompilerMock = (sandbox, statsMock) => ({
const WatchMock = sandbox => ({
close: sandbox.stub().callsFake(cb => cb())
});

const CompilerMock = (sandbox, statsMock, watchMock) => ({
run: sandbox.stub().yields(null, statsMock),
watch: sandbox.stub().yields(null, statsMock)
watch: sandbox.stub().returns(watchMock).yields(null, statsMock)
});

const webpackMock = sandbox => {
const statsMock = StatsMock(sandbox);
const compilerMock = CompilerMock(sandbox, statsMock);
const watchMock = WatchMock(sandbox);
const compilerMock = CompilerMock(sandbox, statsMock, watchMock);
const mock = sinon.stub().returns(compilerMock);
mock.compilerMock = compilerMock;
mock.statsMock = statsMock;
mock.watchMock = watchMock;
return mock;
};

Expand Down
15 changes: 9 additions & 6 deletions tests/wpwatch.test.js
Expand Up @@ -145,23 +145,26 @@ describe('wpwatch', function() {
it('should call callback on subsequent runs', () => {
const wpwatch = module.wpwatch.bind(module);
let watchCallbackSpy;
webpackMock.compilerMock.watch.callsFake((options, cb) => {
webpackMock.compilerMock.watch.onFirstCall().callsFake((options, cb) => {
// We'll spy the callback registered for watch
watchCallbackSpy = sandbox.spy(cb);

// Schedule second call after 2 seconds
setTimeout(() => {
watchCallbackSpy(null, { call: 2 });
process.nextTick(() => watchCallbackSpy(null, { call: 2, hash: '2' }));
}, 2000);
process.nextTick(() => watchCallbackSpy(null, { call: 1 }));
process.nextTick(() => watchCallbackSpy(null, { call: 1, hash: '1' }));
return webpackMock.watchMock;
});
spawnStub.resolves();

return expect(wpwatch()).to.be.fulfilled
.then(() => BbPromise.delay(3000))
.then(() => BbPromise.join(
expect(spawnStub).to.not.have.been.called,
expect(webpackMock.compilerMock.watch).to.have.been.calledOnce,
expect(spawnStub).to.have.been.calledOnce,
expect(spawnStub).to.have.been.calledWithExactly('webpack:compile:watch'),
expect(webpackMock.compilerMock.watch).to.have.been.calledTwice,
expect(webpackMock.watchMock.close).to.have.been.calledOnce,
expect(watchCallbackSpy).to.have.been.calledTwice
));
});
Expand All @@ -181,7 +184,7 @@ describe('wpwatch', function() {
// Ignore the exception. The spy will record it.
}
}, 2000);
process.nextTick(() => watchCallbackSpy(null, { call: 1 }));
process.nextTick(() => watchCallbackSpy(null, { call: 3, hash: '3' }));
});
spawnStub.resolves();

Expand Down