var async = require('async'); var HappyThreadPool = require('./HappyThreadPool'); var HappyForegroundThreadPool = require('./HappyForegroundThreadPool'); var WebpackUtils = require('./WebpackUtils'); var OptionParser = require('./OptionParser'); var JSONSerializer = require('./JSONSerializer'); var SourceMapSerializer = require('./SourceMapSerializer'); var fnOnce = require('./fnOnce'); var ErrorSerializer = require('./ErrorSerializer'); var pick = require('./pick'); var pkg = require('../package.json'); function HappyPlugin(userConfig) { if (!(this instanceof HappyPlugin)) { return new HappyPlugin(userConfig); } this.name = 'HappyPack'; this.state = { loaders: [], baseLoaderRequest: '', foregroundThreadPool: null, verbose: false, debug: false, }; this.config = OptionParser(userConfig, { id: { type: 'string', default: '1' }, compilerId: { type: 'string', default: 'default' }, tempDir: { deprecated: true }, threads: { type: 'number', default: 3 }, threadPool: { type: 'object', default: null }, cache: { deprecated: true }, cachePath: { deprecated: true }, cacheContext: { deprecated: true }, cacheSignatureGenerator: { deprecated: true }, verbose: { type: 'boolean', default: true }, verboseWhenProfiling: { type: 'boolean', default: false }, debug: { type: 'boolean', default: process.env.DEBUG === '1' }, enabled: { deprecated: true }, // we don't want this to be documented / exposed since it's an // implementation detail + not having it on means a bug, but we're making it // configurable for testing purposes bufferedMessaging: { type: 'boolean', default: process.platform === 'win32' }, loaders: { alias: 'use', validate: 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 && !loader.loader; })) { return 'Loader must have a @path or @loader property or be a string.' } } } }, "HappyPack"); this.id = this.config.id; return this; } HappyPlugin.prototype.apply = function(compiler) { var that, engageWatchMode; that = this; this.state.verbose = isVerbose(compiler, this); this.state.debug = isDebug(compiler, this); this.threadPool = this.config.threadPool || HappyThreadPool({ id: this.id, size: this.config.threads, verbose: this.state.verbose, debug: this.state.debug, bufferedMessaging: this.config.bufferedMessaging, }); engageWatchMode = fnOnce(function() { // Once the initial build has completed, we create a foreground worker and // perform all compilations in this thread instead: WebpackUtils.tapInto(compiler, 'done', function() { that.state.foregroundThreadPool = HappyForegroundThreadPool({ loaders: that.state.loaders, }); that.state.foregroundThreadPool.start(that.config.compilerId, compiler, '{}', Function.prototype); }); }); WebpackUtils.tapAsyncInto(compiler, 'watch-run', function(_, done) { if (engageWatchMode() === fnOnce.ALREADY_CALLED) { done(); } else { that.start(compiler, done); } }); WebpackUtils.tapAsyncInto(compiler, 'run', that.start.bind(that)); // cleanup hooks: WebpackUtils.tapInto(compiler, 'done', that.stop.bind(that)); if (compiler.options.bail) { WebpackUtils.tapInto(compiler, 'compilation', function(compilation) { WebpackUtils.tapInto(compilation, 'failed-module', that.stop.bind(that)); }); } }; HappyPlugin.prototype.start = function(compiler, done) { var that = this; if (that.state.verbose) { console.log('Happy[%s]: Version: %s. Threads: %d%s', that.id, pkg.version, that.threadPool.size, that.config.threadPool ? ' (shared pool)' : '' ); } async.series([ function resolveLoaders(callback) { var loaderSpecs = that.config.loaders || that.config.use; var normalLoaders = loaderSpecs.reduce(function(list, entry) { return list.concat(WebpackUtils.normalizeLoader(entry)); }, []); var loaderPaths = normalLoaders.map(function(loader) { return loader.path; }); WebpackUtils.resolveLoaders(compiler, loaderPaths, function(err, resolvedPaths) { if (err) return callback(err); var withResolvedPaths = normalLoaders.map(function(loader, index) { var resolvedPath = resolvedPaths[index]; return Object.assign({}, loader, { path: resolvedPath, request: loader.query ? (loader.path + loader.query) : loader.path }) }) that.state.loaders = withResolvedPaths; that.state.baseLoaderRequest = withResolvedPaths.map(function(loader) { return loader.path + (loader.query || ''); }).join('!'); callback(); }); }, function launchAndConfigureThreads(callback) { var serializedOptions; var compilerOptions = pick(HappyPlugin.SERIALIZABLE_OPTIONS)(compiler.options); try { serializedOptions = JSONSerializer.serialize(compilerOptions); } catch(e) { console.error('Happy[%s]: Internal error; unable to serialize options!!!', that.id); console.error(compilerOptions); return callback(e); } that.threadPool.start(that.config.compilerId, compiler, serializedOptions, callback); }, function announceReadiness(callback) { if (that.state.verbose) { console.log('Happy[%s]: All set; signaling webpack to proceed.', that.id); } callback(); } ], done); }; HappyPlugin.prototype.stop = function() { this.threadPool.stop(this.config.compilerId); }; HappyPlugin.prototype.compile = function(loader, loaderContext, done) { var threadPool = this.state.foregroundThreadPool || this.threadPool; threadPool.compile(loaderContext.remoteLoaderId, loader, { loaders: this.state.loaders, loaderContext: loaderContext, }, function(err, result) { if (err) { done(ErrorSerializer.deserialize(err)); } else { done(null, result.compiledSource || '', SourceMapSerializer.deserialize(result.compiledMap) ); } }); }; 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', ]; // convenience accessor to relieve people from requiring the file directly: HappyPlugin.ThreadPool = HappyThreadPool; function isVerbose(compiler, plugin) { return plugin.config.verbose && ( !compiler.options.profile || plugin.config.verboseWhenProfiling ); }; function isDebug(compiler, plugin) { return plugin.config.debug && ( !compiler.options.profile || plugin.config.verboseWhenProfiling ); }; module.exports = HappyPlugin;