Skip to content
This repository has been archived by the owner on Mar 6, 2024. It is now read-only.

Commit

Permalink
better --watch plugin workflow, less state
Browse files Browse the repository at this point in the history
the "teardown" routine is now idempotent, got rid of much of the
book-keeping state needed between initial/successive builds
  • Loading branch information
amireh committed Apr 9, 2016
1 parent fa0c0d7 commit 5262d0b
Show file tree
Hide file tree
Showing 17 changed files with 247 additions and 132 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
language: node_js
env:
- HAPPY_TEST_TIMEOUT=60000
node_js:
- "stable"
- "4.1"
Expand Down
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,6 @@ as HappyPack will switch into a synchronous mode afterwards (i.e. in `watch`
mode.) Also, if we're using the cache and the compiled versions are indeed
cached, the threads will be idle.

### `installExitHandler: Boolean`

Whether we should intercept the process's `SIGINT` and clean up when it is
received. This is needed because webpack's CLI does not expose any hook for
cleaning up when it is going down, so it's a good idea to hook into it.

You can turn this off if you don't want this functionality or it gives you
trouble.

Defaults to: `true`

## How it works

![A diagram showing the flow between HappyPack's components](doc/HappyPack_Workflow.png)
Expand Down
3 changes: 2 additions & 1 deletion examples/dependency/lib/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
// index.js
// index.js
require('./b');
1 change: 0 additions & 1 deletion examples/dependency/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
var path = require('path');
var through = require('browserify-through');

module.exports = {
entry: path.resolve(__dirname, 'lib/index.js'),
Expand Down
1 change: 1 addition & 0 deletions examples/single-loader/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('./b1');
2 changes: 2 additions & 0 deletions examples/single-loader/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require('./c');
require('./d');
Empty file added examples/single-loader/c.js
Empty file.
1 change: 1 addition & 0 deletions examples/single-loader/d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('success');
3 changes: 3 additions & 0 deletions examples/single-loader/identity-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function(code, map) {
this.callback(null, code, map);
};
28 changes: 28 additions & 0 deletions examples/single-loader/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
var path = require('path');
var HappyPack = require('../../');

module.exports = {
entry: path.resolve(__dirname, 'a.js'),

output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},

plugins: [
new HappyPack({
cache: process.env.HAPPY_CACHE === '1',
loaders: [{ path: path.resolve(__dirname, 'identity-loader.js') }],
threads: 2
})
],

module: {
loaders: [
{
test: /\.js$/,
loader: path.resolve(__dirname, '../../loader')
}
]
}
};
106 changes: 57 additions & 49 deletions lib/HappyPlugin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var fs = require('fs');
var path = require('path');
var async = require('async');
var assert = require('assert');
var HappyThreadPool = require('./HappyThreadPool');
var HappyRPCHandler = require('./HappyRPCHandler');
var HappyFSCache = require('./HappyFSCache');
Expand All @@ -10,6 +11,7 @@ var HappyFakeCompiler = require('./HappyFakeCompiler');
var WebpackUtils = require('./WebpackUtils');
var JSONSerializer = require('./JSONSerializer');
var OptionParser = require('./OptionParser');
var fnOnce = require('./fnOnce');
var pkg = require('../package.json');
var uid = 0;

Expand All @@ -22,8 +24,6 @@ function HappyPlugin(userConfig) {
this.name = 'HappyPack';
this.state = {
started: false,
initialBuildCompleted: false,
compiler: null,
foregroundWorker: null,
};

Expand All @@ -34,7 +34,6 @@ function HappyPlugin(userConfig) {
cache: { type: 'boolean', default: true },
cacheContext: { default: {} },
cachePath: { type: 'string' },
installExitHandler: { type: 'boolean', default: true },

loaders: {
validate: function(value) {
Expand All @@ -52,9 +51,7 @@ function HappyPlugin(userConfig) {
},
isRequired: true,
}
}, this.id);

HappyUtils.mkdirSync(this.config.tempDir);
}, "HappyPack[" + this.id + "]");

if (isNaN(this.config.threads)) {
throw new Error("ArgumentError: Happy[" + this.config.id + "] threads option is invalid.");
Expand All @@ -67,6 +64,8 @@ function HappyPlugin(userConfig) {
path.resolve(this.config.tempDir, 'cache--' + this.id + '.json')
);

HappyUtils.mkdirSync(this.config.tempDir);

return this;
}

Expand All @@ -76,60 +75,47 @@ HappyPlugin.resetUID = function() {

HappyPlugin.prototype.apply = function(compiler) {
var that = this;
var inWatchMode = Boolean(compiler.options.watch);

this.state.compiler = compiler;
var engageWatchMode = fnOnce(function() {
// Once the initial build has completed, we create a foreground worker and
// perform all compilations in this thread instead:
compiler.plugin('done', function() {
that.state.foregroundWorker = createForegroundWorker(compiler, that.config.loaders);
});

compiler.plugin('run', start);
compiler.plugin('watch-run', start);
// TODO: anything special to do here?
compiler.plugin('failed', function(err) {
console.warn('fatal watch error!!!', err);
});
});

compiler.plugin('compilation', function(compilation) {
if (compiler.options.bail) {
compilation.plugin('failed-module', teardown);
compiler.plugin('watch-run', function(_, done) {
if (engageWatchMode() === fnOnce.ALREADY_CALLED) {
done();
}
else {
that.start(compiler, done);
}
});

compiler.plugin('done', function(/*stats*/) {
teardown();
compiler.plugin('run', that.start.bind(that));

that.state.initialBuildCompleted = true;
that.state.foregroundWorker = createForegroundWorker(compiler, that.config.loaders);
});
// cleanup hooks:
compiler.plugin('done', that.stop.bind(that));

if (inWatchMode && this.config.installExitHandler !== false) {
process.on('exit', teardown);
process.on('SIGINT', function() {
process.exit(0);
if (compiler.options.bail) {
compiler.plugin('compilation', function(compilation) {
compilation.plugin('failed-module', that.stop.bind(that));
});
}

function start(_, done) {
that.start(compiler, done);
}

function teardown() {
if (that.state.started) {
if (that.config.cache) {
that.cache.save();
}

if (that.threadPool) {
that.threadPool.stop();
}

that.state.started = false;
}
}
};

HappyPlugin.prototype.start = function(compiler, done) {
var that = this;

if (that.state.started || that.state.initialBuildCompleted) {
return done();
}
assert(!that.state.started, "HappyPlugin has already been started!");

console.log('Happy[%s]: Firing up! Version: %s. Using cache? %s. Threads: %d',
console.log('Happy[%s]: Version: %s. Using cache? %s. Threads: %d',
that.id, pkg.version, that.config.cache ? 'yes' : 'no', that.config.threads);

async.series([
Expand Down Expand Up @@ -183,7 +169,6 @@ HappyPlugin.prototype.start = function(compiler, done) {
callback();
},

// TODO: accept a config file path
function serializeOptions(callback) {
// serialize the options so that the workers can pick them up
try {
Expand Down Expand Up @@ -218,8 +203,21 @@ HappyPlugin.prototype.start = function(compiler, done) {
], done);
};

HappyPlugin.prototype.stop = function() {
assert(this.state.started, "HappyPlugin can not be torn down until started!");

if (this.config.cache) {
this.cache.save();
}

if (this.threadPool) {
this.threadPool.stop();
this.threadPool = null;
}
};

HappyPlugin.prototype.compile = function(loaderContext, done) {
if (this.state.initialBuildCompleted) {
if (this.state.foregroundWorker) {
return this.compileInForeground(loaderContext, done);
}
else {
Expand Down Expand Up @@ -277,16 +275,25 @@ HappyPlugin.prototype.generateRequest = function(resource) {

// export this so that users get to override if needed
HappyPlugin.SERIALIZABLE_OPTIONS = [
'amd',
'bail',
'cache',
'context',
'entry',
'externals',
'debug',
'devtool',
'resolve',
'resolveLoader',
'devServer',
'loader',
'module',
'node',
'output',
'profile',
'recordsPath',
'recordsInputPath',
'recordsOutputPath',
'resolve',
'resolveLoader',
'target',
'watch',
];
Expand All @@ -304,7 +311,7 @@ HappyPlugin.extractCompilerOptions = function(options) {
};

function createForegroundWorker(compiler, loaders) {
var fakeCompiler = new HappyFakeCompiler('foreground', function applyCompilerRPC(message) {
var fakeCompiler = new HappyFakeCompiler('foreground', function executeCompilerRPC(message) {
// TODO: DRY alert, see HappyThread.js
HappyRPCHandler.execute(message.data.type, message.data.payload, function serveRPCResult(error, result) {
fakeCompiler._handleResponse({
Expand All @@ -319,4 +326,5 @@ function createForegroundWorker(compiler, loaders) {

return new HappyWorker({ compiler: fakeCompiler, loaders: loaders });
}

module.exports = HappyPlugin;
5 changes: 4 additions & 1 deletion lib/HappyTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ exports.clearCache = function() {
};

exports.IntegrationSuite = function(mochaSuite) {
mochaSuite.timeout(60000); // for travis
mochaSuite.timeout(process.env.HAPPY_TEST_TIMEOUT ?
parseInt(process.env.HAPPY_TEST_TIMEOUT, 10) :
2000
);

mochaSuite.beforeEach(function() {
HappyPlugin.resetUID();
Expand Down
2 changes: 1 addition & 1 deletion lib/OptionParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ module.exports = function parseAndValidateOptions(params, schema, displayName) {
}

function format(message) {
return "HappyPack[" + displayName + "]: " + message;
return displayName + ": " + message;
}
};
66 changes: 0 additions & 66 deletions lib/__tests__/HappyPlugin.integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,72 +178,6 @@ describe('[Integration] HappyPlugin', function() {
});
});

it('works with watch mode', function(done) {
const outputDir = tempDir('integration-[guid]');

const loader = createLoader(function(s) {
this.addDependency(this.resource.replace('.js', '.tmp.js'));

return s + '\n// HAPPY!';
});

const file1 = TestUtils.createFile('a.js', "require('./b.js');");

const file2 = TestUtils.createFile('b.js', '');

const compiler = webpack({
entry: {
main: file1.getPath()
},

output: {
filename: '[name].js',
path: outputDir
},

module: {
loaders: [{
test: /.js$/,
loader: path.resolve(__dirname, '../HappyLoader.js')
}]
},

plugins: [
new HappyPlugin({
cache: false,
loaders: [loader.path]
})
]
});

let runCount = 0;

var watcher = compiler.watch({}, function(err, rawStats) {
if (err) { return done(err); }

const stats = rawStats.toJson();

if (stats.errors.length > 0) {
return done(stats.errors);
}

assert.match(fs.readFileSync(path.join(outputDir, 'main.js'), 'utf-8'), '// HAPPY');

process.stdout.write(rawStats.toString({}) + "\n");

if (++runCount === 3) {
watcher.close(function() {
done();
});
}
else {
setTimeout(function() {
fs.appendFileSync(file2.getPath(), '\nconsole.log("foo");', 'utf-8');
}, 500);
}
});
});

// TODO pitch loader support
it.skip('works with multiple plugins', function(done) {
const outputDir = tempDir('integration-[guid]');
Expand Down
Loading

0 comments on commit 5262d0b

Please sign in to comment.