1 | var fs = require('fs');
|
2 | var path = require('path');
|
3 | var async = require('async');
|
4 | var assert = require('assert');
|
5 | var HappyThreadPool = require('./HappyThreadPool');
|
6 | var HappyRPCHandler = require('./HappyRPCHandler');
|
7 | var HappyFSCache = require('./HappyFSCache');
|
8 | var HappyUtils = require('./HappyUtils');
|
9 | var HappyWorker = require('./HappyWorker');
|
10 | var HappyFakeCompiler = require('./HappyFakeCompiler');
|
11 | var WebpackUtils = require('./WebpackUtils');
|
12 | var OptionParser = require('./OptionParser');
|
13 | var JSONSerializer = require('./JSONSerializer');
|
14 | var SourceMapSerializer = require('./SourceMapSerializer');
|
15 | var fnOnce = require('./fnOnce');
|
16 | var pkg = require('../package.json');
|
17 | var uid = 0;
|
18 |
|
19 | function 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 |
|
79 | HappyPlugin.resetUID = function() {
|
80 | uid = 0;
|
81 | };
|
82 |
|
83 | HappyPlugin.prototype.apply = function(compiler) {
|
84 | if (this.config.enabled === false) {
|
85 | return;
|
86 | }
|
87 |
|
88 | var that = this;
|
89 | var engageWatchMode = fnOnce(function() {
|
90 |
|
91 |
|
92 | compiler.plugin('done', function() {
|
93 | that.state.foregroundWorker = createForegroundWorker(compiler, that.state.loaders);
|
94 | });
|
95 |
|
96 |
|
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 |
|
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 |
|
123 | HappyPlugin.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 |
|
146 |
|
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 |
|
156 | sourceLoaderConfig.loader = path.resolve(__dirname, 'HappyLoader.js') + '?id=' + that.id;
|
157 | delete sourceLoaderConfig.query;
|
158 | delete sourceLoaderConfig.loaders;
|
159 |
|
160 |
|
161 |
|
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 |
|
240 | HappyPlugin.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 |
|
250 | HappyPlugin.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 |
|
259 | HappyPlugin.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 |
|
276 | HappyPlugin.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 |
|
293 |
|
294 | HappyPlugin.prototype.compileInForeground = function(loaderContext, done) {
|
295 | this._performCompilationRequest(this.state.foregroundWorker, loaderContext, done);
|
296 | };
|
297 |
|
298 | HappyPlugin.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 |
|
327 | HappyPlugin.prototype.generateRequest = function(resource) {
|
328 | return this.state.baseLoaderRequest + '!' + resource;
|
329 | };
|
330 |
|
331 |
|
332 | HappyPlugin.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 |
|
356 | HappyPlugin.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 |
|
368 | function createForegroundWorker(compiler, loaders) {
|
369 | var fakeCompiler = new HappyFakeCompiler('foreground', function executeCompilerRPC(message) {
|
370 |
|
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 |
|
388 | HappyPlugin.ThreadPool = HappyThreadPool;
|
389 |
|
390 | module.exports = HappyPlugin;
|