UNPKG

happypack

Version:

webpack speed booster, makes you happy!

359 lines (296 loc) 10.3 kB
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'); var HappyUtils = require('./HappyUtils'); var HappyWorker = require('./HappyWorker'); var HappyFakeCompiler = require('./HappyFakeCompiler'); var WebpackUtils = require('./WebpackUtils'); var OptionParser = require('./OptionParser'); var JSONSerializer = require('./JSONSerializer'); var fnOnce = require('./fnOnce'); var pkg = require('../package.json'); var uid = 0; function HappyPlugin(userConfig) { if (!(this instanceof HappyPlugin)) { return new HappyPlugin(userConfig); } this.id = String(userConfig.id || ++uid); this.name = 'HappyPack'; this.state = { started: false, loaders: [], baseLoaderRequest: '', foregroundWorker: null, }; this.config = OptionParser(userConfig, { id: { type: 'string' }, tempDir: { type: 'string', default: '.happypack' }, threads: { type: 'number', default: 3 }, threadPool: { type: 'object', default: null }, cache: { type: 'boolean', default: true }, cacheContext: { default: {} }, cachePath: { type: 'string' }, inferLoaders: { type: 'boolean', default: false }, verbose: { type: 'boolean', default: true }, enabled: { type: 'boolean', default: true }, loaders: { validate: function(value) { if (!Array.isArray(value)) { return 'Loaders must be an array!'; } else if (value.length === 0) { return 'You must specify at least one loader!'; } else if (value.some(function(loader) { return typeof loader !== 'string' && !loader.path; })) { return 'Loader must have a @path property or be a string.' } }, } }, "HappyPack[" + this.id + "]"); this.threadPool = this.config.threadPool || HappyThreadPool({ size: this.config.threads }); this.cache = HappyFSCache(this.id, this.config.cachePath ? path.resolve(this.config.cachePath.replace(/\[id\]/g, this.id)) : path.resolve(this.config.tempDir, 'cache--' + this.id + '.json'), this.config.verbose ); HappyUtils.mkdirSync(this.config.tempDir); return this; } HappyPlugin.resetUID = function() { uid = 0; }; HappyPlugin.prototype.apply = function(compiler) { if (this.config.enabled === false) { return; } var that = this; 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.state.loaders); }); // TODO: anything special to do here? compiler.plugin('failed', function(err) { console.warn('fatal watch error!!!', err); }); }); compiler.plugin('watch-run', function(_, done) { if (engageWatchMode() === fnOnce.ALREADY_CALLED) { done(); } else { that.start(compiler, done); } }); compiler.plugin('run', that.start.bind(that)); // cleanup hooks: compiler.plugin('done', that.stop.bind(that)); if (compiler.options.bail) { compiler.plugin('compilation', function(compilation) { compilation.plugin('failed-module', that.stop.bind(that)); }); } }; HappyPlugin.prototype.start = function(compiler, done) { var that = this; assert(!that.state.started, "HappyPlugin has already been started!"); if (that.config.verbose) { console.log('Happy[%s]: Version: %s. Using cache? %s. Threads: %d%s', that.id, pkg.version, that.config.cache ? 'yes' : 'no', that.threadPool.size, that.config.threadPool ? ' (shared pool)' : '' ); } async.series([ function registerCompilerForRPCs(callback) { HappyRPCHandler.registerActiveCompiler(compiler); callback(); }, function normalizeLoaders(callback) { var loaders = that.config.loaders; // if no loaders are configured, try to infer from existing module.loaders // list if any entry has a "{ happy: { id: ... } }" object if (!loaders) { var sourceLoaderConfig = compiler.options.module.loaders.filter(function(loader) { return loader.happy && loader.happy.id === that.id; })[0]; if (sourceLoaderConfig) { loaders = sourceLoaderConfig.loaders || [ sourceLoaderConfig.loader ]; // yeah, yuck... we need to overwrite, ugly! sourceLoaderConfig.loader = path.resolve(__dirname, 'HappyLoader.js') + '?id=' + that.id; } } assert(loaders, "HappyPlugin[" + that.id + "]; you have not specified any loaders " + "and there is no matching loader entry with this id either." ); that.state.loaders = loaders .map(WebpackUtils.disectLoaderString) .reduce(function(allLoaders, loadersFoundInString) { return allLoaders.concat(loadersFoundInString); }, []) ; callback(null); }, function resolveLoaders(callback) { var loaderPaths = that.state.loaders.map(function(loader) { if (loader.query) { return loader.path + loader.query; } return loader.path; }); WebpackUtils.resolveLoaders(compiler, loaderPaths, function(err, loaders) { if (err) return callback(err); that.state.loaders = loaders; that.state.baseLoaderRequest = loaders.map(function(loader) { return loader.path + (loader.query || ''); }).join('!'); callback(); }); }, function loadCache(callback) { if (that.config.cache) { that.cache.load({ loaders: that.state.loaders, external: that.config.cacheContext }); } callback(); }, function launchAndConfigureThreads(callback) { that.threadPool.start(function() { var serializedOptions; var compilerOptions = HappyPlugin.extractCompilerOptions(compiler.options); try { serializedOptions = JSONSerializer.serialize(compilerOptions); } catch(e) { console.error('Happy[%s]: Unable to serialize options!!! This is an internal error.', that.id); console.error(compilerOptions); return callback(e); } that.threadPool.configure(serializedOptions, callback); }); }, function markStarted(callback) { console.log('Happy[%s]: All set; signalling webpack to proceed.', that.id); that.state.started = true; callback(); } ], done); }; HappyPlugin.prototype.stop = function() { assert(this.state.started, "HappyPlugin can not be torn down until started!"); if (this.config.cache) { this.cache.save(); } this.threadPool.stop(); }; HappyPlugin.prototype.compile = function(loaderContext, done) { if (this.state.foregroundWorker) { return this.compileInForeground(loaderContext, done); } else { return this.compileInBackground(loaderContext, done); } }; HappyPlugin.prototype.compileInBackground = function(loaderContext, done) { var cache = this.cache; var filePath = loaderContext.resourcePath; if (!cache.hasChanged(filePath) && !cache.hasErrored(filePath)) { return done(null, fs.readFileSync(cache.getCompiledPath(filePath), 'utf-8')); } if (process.env.HAPPY_DEBUG) { console.warn('File had changed, re-compiling... (%s)', filePath); } this._performCompilationRequest(this.threadPool.get(), loaderContext, done); }; // compile the source using the foreground worker instead of sending to the // background threads: HappyPlugin.prototype.compileInForeground = function(loaderContext, done) { this._performCompilationRequest(this.state.foregroundWorker, loaderContext, done); }; HappyPlugin.prototype._performCompilationRequest = function(worker, loaderContext, done) { var cache = this.cache; var filePath = loaderContext.resourcePath; cache.invalidateEntryFor(filePath); worker.compile({ loaders: this.state.loaders, compiledPath: path.resolve(this.config.tempDir, HappyUtils.generateCompiledPath(filePath)), loaderContext: loaderContext, }, function(result) { var contents = fs.readFileSync(result.compiledPath, 'utf-8') if (!result.success) { cache.updateMTimeFor(filePath, null, contents); done(contents); } else { cache.updateMTimeFor(filePath, result.compiledPath); done(null, contents, null /* TODO: SourceMaps */); } }); }; HappyPlugin.prototype.generateRequest = function(resource) { return this.state.baseLoaderRequest + '!' + resource; }; // export this so that users get to override if needed HappyPlugin.SERIALIZABLE_OPTIONS = [ 'amd', 'bail', 'cache', 'context', 'entry', 'externals', 'debug', 'devtool', 'devServer', 'loader', 'module', 'node', 'output', 'profile', 'recordsPath', 'recordsInputPath', 'recordsOutputPath', 'resolve', 'resolveLoader', 'target', 'watch', ]; HappyPlugin.extractCompilerOptions = function(options) { var ALLOWED_KEYS = HappyPlugin.SERIALIZABLE_OPTIONS; return Object.keys(options).reduce(function(hsh, key) { if (ALLOWED_KEYS.indexOf(key) > -1) { hsh[key] = options[key]; } return hsh; }, {}); }; function createForegroundWorker(compiler, loaders) { 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({ id: message.data.id, payload: { error: error || null, result: result || null } }); }); }, compiler.options); return new HappyWorker({ compiler: fakeCompiler, loaders: loaders }); } // convenience accessor to relieve people from requiring the file directly: HappyPlugin.ThreadPool = HappyThreadPool; module.exports = HappyPlugin;