UNPKG

11.3 kBJavaScriptView Raw
1var fs = require('fs');
2var path = require('path');
3var async = require('async');
4var assert = require('assert');
5var HappyThreadPool = require('./HappyThreadPool');
6var HappyRPCHandler = require('./HappyRPCHandler');
7var HappyFSCache = require('./HappyFSCache');
8var HappyUtils = require('./HappyUtils');
9var HappyWorker = require('./HappyWorker');
10var HappyFakeCompiler = require('./HappyFakeCompiler');
11var WebpackUtils = require('./WebpackUtils');
12var OptionParser = require('./OptionParser');
13var JSONSerializer = require('./JSONSerializer');
14var SourceMapSerializer = require('./SourceMapSerializer');
15var fnOnce = require('./fnOnce');
16var pkg = require('../package.json');
17var uid = 0;
18
19function HappyPlugin(userConfig) {
20 if (!(this instanceof HappyPlugin)) {
21 return new HappyPlugin(userConfig);
22 }
23
24 this.id = String(userConfig.id || ++uid);
25 this.name = 'HappyPack';
26 this.state = {
27 started: false,
28 loaders: [],
29 baseLoaderRequest: '',
30 foregroundWorker: null,
31 };
32
33 this.config = OptionParser(userConfig, {
34 id: { type: 'string' },
35 tempDir: { type: 'string', default: '.happypack' },
36 threads: { type: 'number', default: 3 },
37 threadPool: { type: 'object', default: null },
38 cache: { type: 'boolean', default: true },
39 cachePath: { type: 'string' },
40 cacheContext: { type: 'object', default: {} },
41 cacheSignatureGenerator: { type: 'function' },
42 verbose: { type: 'boolean', default: true },
43 enabled: { type: 'boolean', default: true },
44 loaders: {
45 validate: function(value) {
46 if (!Array.isArray(value)) {
47 return 'Loaders must be an array!';
48 }
49 else if (value.length === 0) {
50 return 'You must specify at least one loader!';
51 }
52 else if (value.some(function(loader) {
53 return typeof loader !== 'string' && !loader.path;
54 })) {
55 return 'Loader must have a @path property or be a string.'
56 }
57 },
58 }
59 }, "HappyPack[" + this.id + "]");
60
61 this.threadPool = this.config.threadPool || HappyThreadPool({
62 size: this.config.threads
63 });
64
65 this.cache = HappyFSCache({
66 id: this.id,
67 path: this.config.cachePath ?
68 path.resolve(this.config.cachePath.replace(/\[id\]/g, this.id)) :
69 path.resolve(this.config.tempDir, 'cache--' + this.id + '.json'),
70 verbose: this.config.verbose,
71 generateSignature: this.config.cacheSignatureGenerator
72 });
73
74 HappyUtils.mkdirSync(this.config.tempDir);
75
76 return this;
77}
78
79HappyPlugin.resetUID = function() {
80 uid = 0;
81};
82
83HappyPlugin.prototype.apply = function(compiler) {
84 if (this.config.enabled === false) {
85 return;
86 }
87
88 var that = this;
89 var engageWatchMode = fnOnce(function() {
90 // Once the initial build has completed, we create a foreground worker and
91 // perform all compilations in this thread instead:
92 compiler.plugin('done', function() {
93 that.state.foregroundWorker = createForegroundWorker(compiler, that.state.loaders);
94 });
95
96 // TODO: anything special to do here?
97 compiler.plugin('failed', function(err) {
98 console.warn('fatal watch error!!!', err);
99 });
100 });
101
102 compiler.plugin('watch-run', function(_, done) {
103 if (engageWatchMode() === fnOnce.ALREADY_CALLED) {
104 done();
105 }
106 else {
107 that.start(compiler, done);
108 }
109 });
110
111 compiler.plugin('run', that.start.bind(that));
112
113 // cleanup hooks:
114 compiler.plugin('done', that.stop.bind(that));
115
116 if (compiler.options.bail) {
117 compiler.plugin('compilation', function(compilation) {
118 compilation.plugin('failed-module', that.stop.bind(that));
119 });
120 }
121};
122
123HappyPlugin.prototype.start = function(compiler, done) {
124 var that = this;
125
126 if (that.config.verbose) {
127 console.log('Happy[%s]: Version: %s. Using cache? %s. Threads: %d%s',
128 that.id, pkg.version,
129 that.config.cache ? 'yes' : 'no',
130 that.threadPool.size,
131 that.config.threadPool ? ' (shared pool)' : ''
132 );
133 }
134
135 async.series([
136 function registerCompilerForRPCs(callback) {
137 HappyRPCHandler.registerActiveCompiler(compiler);
138
139 callback();
140 },
141
142 function normalizeLoaders(callback) {
143 var loaders = that.config.loaders;
144
145 // if no loaders are configured, try to infer from existing module.loaders
146 // list if any entry has a "{ happy: { id: ... } }" object
147 if (!loaders) {
148 var sourceLoaderConfig = compiler.options.module.loaders.filter(function(loader) {
149 return loader.happy && loader.happy.id === that.id;
150 })[0];
151
152 if (sourceLoaderConfig) {
153 loaders = WebpackUtils.extractLoaders(sourceLoaderConfig);
154
155 // yeah, yuck... we need to overwrite, ugly!
156 sourceLoaderConfig.loader = path.resolve(__dirname, 'HappyLoader.js') + '?id=' + that.id;
157 delete sourceLoaderConfig.query;
158 delete sourceLoaderConfig.loaders;
159
160 // TODO: it would be nice if we can restore those mutations at some
161 // point
162 }
163 }
164
165 assert(loaders && loaders.length > 0,
166 "HappyPlugin[" + that.id + "]; you have not specified any loaders " +
167 "and there is no matching loader entry with this id either."
168 );
169
170 that.state.loaders = loaders
171 .map(WebpackUtils.disectLoaderString)
172 .reduce(function(allLoaders, loadersFoundInString) {
173 return allLoaders.concat(loadersFoundInString);
174 }, [])
175 ;
176
177 callback(null);
178 },
179
180 function resolveLoaders(callback) {
181 var loaderPaths = that.state.loaders.map(function(loader) {
182 if (loader.query) {
183 return loader.path + loader.query;
184 }
185 return loader.path;
186 });
187
188 WebpackUtils.resolveLoaders(compiler, loaderPaths, function(err, loaders) {
189 if (err) return callback(err);
190
191 that.state.loaders = loaders;
192 that.state.baseLoaderRequest = loaders.map(function(loader) {
193 return loader.path + (loader.query || '');
194 }).join('!');
195
196 callback();
197 });
198 },
199
200 function loadCache(callback) {
201 if (that.config.cache) {
202 that.cache.load({
203 loaders: that.state.loaders,
204 external: that.config.cacheContext
205 });
206 }
207
208 callback();
209 },
210
211 function launchAndConfigureThreads(callback) {
212 that.threadPool.start(function() {
213 var serializedOptions;
214 var compilerOptions = HappyPlugin.extractCompilerOptions(compiler.options);
215
216 try {
217 serializedOptions = JSONSerializer.serialize(compilerOptions);
218 }
219 catch(e) {
220 console.error('Happy[%s]: Unable to serialize options!!! This is an internal error.', that.id);
221 console.error(compilerOptions);
222
223 return callback(e);
224 }
225
226 that.threadPool.configure(serializedOptions, callback);
227 });
228 },
229
230 function markStarted(callback) {
231 console.log('Happy[%s]: All set; signalling webpack to proceed.', that.id);
232
233 that.state.started = true;
234
235 callback();
236 }
237 ], done);
238};
239
240HappyPlugin.prototype.stop = function() {
241 assert(this.state.started, "HappyPlugin can not be torn down until started!");
242
243 if (this.config.cache) {
244 this.cache.save();
245 }
246
247 this.threadPool.stop();
248};
249
250HappyPlugin.prototype.compile = function(loaderContext, done) {
251 if (this.state.foregroundWorker) {
252 return this.compileInForeground(loaderContext, done);
253 }
254 else {
255 return this.compileInBackground(loaderContext, done);
256 }
257};
258
259HappyPlugin.prototype.compileInBackground = function(loaderContext, done) {
260 var cache = this.cache;
261 var filePath = loaderContext.resourcePath;
262
263 if (!cache.hasChanged(filePath) && !cache.hasErrored(filePath)) {
264 var cached = this.readFromCache(filePath);
265
266 return done(null, cached.sourceCode, cached.sourceMap);
267 }
268
269 if (process.env.HAPPY_DEBUG) {
270 console.warn('File had changed, re-compiling... (%s)', filePath);
271 }
272
273 this._performCompilationRequest(this.threadPool.get(), loaderContext, done);
274};
275
276HappyPlugin.prototype.readFromCache = function(filePath) {
277 var cached = {};
278 var sourceCodeFilePath = this.cache.getCompiledSourceCodePath(filePath);
279 var sourceMapFilePath = this.cache.getCompiledSourceMapPath(filePath);
280
281 cached.sourceCode = fs.readFileSync(sourceCodeFilePath, 'utf-8');
282
283 if (HappyUtils.isReadable(sourceMapFilePath)) {
284 cached.sourceMap = SourceMapSerializer.deserialize(
285 fs.readFileSync(sourceMapFilePath, 'utf-8')
286 );
287 }
288
289 return cached;
290};
291
292// compile the source using the foreground worker instead of sending to the
293// background threads:
294HappyPlugin.prototype.compileInForeground = function(loaderContext, done) {
295 this._performCompilationRequest(this.state.foregroundWorker, loaderContext, done);
296};
297
298HappyPlugin.prototype._performCompilationRequest = function(worker, loaderContext, done) {
299 var cache = this.cache;
300 var filePath = loaderContext.resourcePath;
301
302 cache.invalidateEntryFor(filePath);
303
304 worker.compile({
305 loaders: this.state.loaders,
306 compiledPath: path.resolve(this.config.tempDir, HappyUtils.generateCompiledPath(filePath)),
307 loaderContext: loaderContext,
308 }, function(result) {
309 var contents = fs.readFileSync(result.compiledPath, 'utf-8')
310 var compiledMap;
311
312 if (!result.success) {
313 cache.updateMTimeFor(filePath, null, contents);
314 done(contents);
315 }
316 else {
317 cache.updateMTimeFor(filePath, result.compiledPath);
318 compiledMap = SourceMapSerializer.deserialize(
319 fs.readFileSync(cache.getCompiledSourceMapPath(filePath), 'utf-8')
320 );
321
322 done(null, contents, compiledMap);
323 }
324 });
325};
326
327HappyPlugin.prototype.generateRequest = function(resource) {
328 return this.state.baseLoaderRequest + '!' + resource;
329};
330
331// export this so that users get to override if needed
332HappyPlugin.SERIALIZABLE_OPTIONS = [
333 'amd',
334 'bail',
335 'cache',
336 'context',
337 'entry',
338 'externals',
339 'debug',
340 'devtool',
341 'devServer',
342 'loader',
343 'module',
344 'node',
345 'output',
346 'profile',
347 'recordsPath',
348 'recordsInputPath',
349 'recordsOutputPath',
350 'resolve',
351 'resolveLoader',
352 'target',
353 'watch',
354];
355
356HappyPlugin.extractCompilerOptions = function(options) {
357 var ALLOWED_KEYS = HappyPlugin.SERIALIZABLE_OPTIONS;
358
359 return Object.keys(options).reduce(function(hsh, key) {
360 if (ALLOWED_KEYS.indexOf(key) > -1) {
361 hsh[key] = options[key];
362 }
363
364 return hsh;
365 }, {});
366};
367
368function createForegroundWorker(compiler, loaders) {
369 var fakeCompiler = new HappyFakeCompiler('foreground', function executeCompilerRPC(message) {
370 // TODO: DRY alert, see HappyThread.js
371 HappyRPCHandler.execute(message.data.type, message.data.payload, function serveRPCResult(error, result) {
372 fakeCompiler._handleResponse({
373 id: message.data.id,
374 payload: {
375 error: error || null,
376 result: result || null
377 }
378 });
379 });
380 });
381
382 fakeCompiler.options = compiler.options;
383
384 return new HappyWorker({ compiler: fakeCompiler, loaders: loaders });
385}
386
387// convenience accessor to relieve people from requiring the file directly:
388HappyPlugin.ThreadPool = HappyThreadPool;
389
390module.exports = HappyPlugin;