UNPKG

8.31 kBJavaScriptView Raw
1/** Copyright (c) 2018 Uber Technologies, Inc.
2 *
3 * This source code is licensed under the MIT license found in the
4 * LICENSE file in the root directory of this source tree.
5 *
6 * @flow
7 */
8
9/* eslint-env node */
10
11const fs = require('fs');
12const path = require('path');
13
14const webpack = require('webpack');
15const chalk = require('chalk');
16const webpackHotMiddleware = require('webpack-hot-middleware');
17const rimraf = require('rimraf');
18
19const webpackDevMiddleware = require('../lib/simple-webpack-dev-middleware');
20const {getWebpackConfig} = require('./get-webpack-config.js');
21const {
22 DeferredState,
23 SyncState,
24 MergedDeferredState,
25} = require('./shared-state-containers.js');
26const mergeChunkMetadata = require('./merge-chunk-metadata');
27const loadFusionRC = require('./load-fusionrc.js');
28
29const Worker = require('jest-worker').default;
30
31function getErrors(info) {
32 let errors = [].concat(info.errors);
33 if (info.children.length) {
34 errors = errors.concat(
35 info.children.reduce((x, child) => {
36 return x.concat(getErrors(child));
37 }, [])
38 );
39 }
40 return dedupeErrors(errors);
41}
42
43function getWarnings(info) {
44 let warnings = [].concat(info.warnings);
45 if (info.children.length) {
46 warnings = warnings.concat(
47 info.children.reduce((x, child) => {
48 return x.concat(getWarnings(child));
49 }, [])
50 );
51 }
52 return dedupeErrors(warnings);
53}
54
55function dedupeErrors(items) {
56 const re = /BabelLoaderError(.|\n)+( {4}at transpile)/gim;
57 const set = new Set(items.map(item => item.replace(re, '$2')));
58 return Array.from(set);
59}
60
61function getStatsLogger({dir, logger, env}) {
62 return (err, stats) => {
63 // syntax errors are logged 4 times (once by webpack, once by babel, once on server and once on client)
64 // we only want to log each syntax error once
65 const isProd = env === 'production';
66
67 if (err) {
68 logger.error(err.stack || err);
69 if (err.details) {
70 logger.error(err.details);
71 }
72 return;
73 }
74
75 const file = path.resolve(dir, '.fusion/stats.json');
76 const info = stats.toJson({context: path.resolve(dir)});
77 fs.writeFile(file, JSON.stringify(info, null, 2), () => {});
78
79 if (stats.hasErrors()) {
80 getErrors(info).forEach(e => logger.error(e));
81 }
82 // TODO(#13): These logs seem to be kinda noisy for dev.
83 if (isProd) {
84 info.children.forEach(child => {
85 child.assets
86 .slice()
87 .filter(asset => {
88 return !asset.name.endsWith('.map');
89 })
90 .sort((a, b) => {
91 return b.size - a.size;
92 })
93 .forEach(asset => {
94 logger.info(`Entrypoint: ${chalk.bold(child.name)}`);
95 logger.info(`Asset: ${chalk.bold(asset.name)}`);
96 logger.info(`Size: ${chalk.bold(asset.size)} bytes`);
97 });
98 });
99 }
100 if (stats.hasWarnings()) {
101 getWarnings(info).forEach(e => logger.warn(e));
102 }
103 };
104}
105
106/*::
107type CompilerType = {
108 on: (type: any, callback: any) => any,
109 start: (callback: any) => any,
110 getMiddleware: () => any,
111 clean: () => any,
112};
113*/
114
115/*::
116type CompilerOpts = {
117 serverless?: boolean,
118 dir?: string,
119 env: "production" | "development",
120 hmr?: boolean,
121 watch?: boolean,
122 forceLegacyBuild?: boolean,
123 logger?: any,
124 preserveNames?: boolean,
125 minify?: boolean,
126 modernBuildOnly?: boolean,
127 maxWorkers?: number,
128 skipSourceMaps?: boolean,
129};
130*/
131
132function Compiler(
133 {
134 dir = '.',
135 env,
136 hmr = true,
137 forceLegacyBuild,
138 preserveNames,
139 watch = false,
140 logger = console,
141 minify = true,
142 serverless = false,
143 modernBuildOnly = false,
144 skipSourceMaps = false,
145 maxWorkers,
146 } /*: CompilerOpts */
147) /*: CompilerType */ {
148 const root = path.resolve(dir);
149 const fusionConfig = loadFusionRC(root);
150 const legacyPkgConfig = loadLegacyPkgConfig(root);
151
152 const clientChunkMetadata = new DeferredState();
153 const legacyClientChunkMetadata = new DeferredState();
154 const legacyBuildEnabled = new SyncState(
155 (forceLegacyBuild || !watch || env === 'production') &&
156 !(modernBuildOnly || fusionConfig.modernBuildOnly)
157 );
158 const mergedClientChunkMetadata /*: any */ = new MergedDeferredState(
159 [
160 {deferred: clientChunkMetadata, enabled: new SyncState(true)},
161 {deferred: legacyClientChunkMetadata, enabled: legacyBuildEnabled},
162 ],
163 mergeChunkMetadata
164 );
165
166 const state = {
167 clientChunkMetadata,
168 legacyClientChunkMetadata,
169 mergedClientChunkMetadata,
170 i18nManifest: new Map(),
171 i18nDeferredManifest: new DeferredState(),
172 legacyBuildEnabled,
173 };
174
175 let worker = createWorker(maxWorkers);
176
177 const sharedOpts = {
178 dir: root,
179 dev: env === 'development',
180 hmr,
181 watch,
182 state,
183 fusionConfig,
184 legacyPkgConfig,
185 skipSourceMaps,
186 preserveNames,
187 // TODO: Remove redundant zopfli option
188 zopfli: fusionConfig.zopfli != undefined ? fusionConfig.zopfli : true,
189 gzip: fusionConfig.gzip != undefined ? fusionConfig.gzip : true,
190 brotli: fusionConfig.brotli != undefined ? fusionConfig.brotli : true,
191 minify,
192 worker,
193 };
194 const compiler = webpack([
195 getWebpackConfig({id: 'client-modern', ...sharedOpts}),
196 getWebpackConfig({
197 id: serverless ? 'serverless' : 'server',
198 ...sharedOpts,
199 }),
200 ]);
201 if (process.env.LOG_END_TIME == 'true') {
202 compiler.hooks.done.tap('BenchmarkTimingPlugin', stats => {
203 /* eslint-disable-next-line no-console */
204 console.log(`End time: ${Date.now()}`);
205 });
206 }
207
208 if (watch) {
209 compiler.hooks.watchRun.tap('StartWorkersAgain', () => {
210 if (worker === void 0) worker = createWorker(maxWorkers);
211 });
212 compiler.hooks.watchClose.tap('KillWorkers', stats => {
213 if (worker !== void 0) worker.end();
214 worker = void 0;
215 });
216 } else
217 compiler.hooks.done.tap('KillWorkers', stats => {
218 if (worker !== void 0) worker.end();
219 worker = void 0;
220 });
221
222 const statsLogger = getStatsLogger({dir, logger, env});
223
224 this.on = (type, callback) => compiler.hooks[type].tap('compiler', callback);
225 this.start = cb => {
226 cb = cb || function noop(err, stats) {};
227 // Handler may be called multiple times by `watch`
228 // But only call `cb` the first time
229 // subsequent rebuilds are subscribed to with 'compiler.on('done')'
230 let hasCalledCb = false;
231 const handler = (err, stats) => {
232 statsLogger(err, stats);
233 if (!hasCalledCb) {
234 hasCalledCb = true;
235 cb(err, stats);
236 }
237 };
238 if (watch) {
239 return compiler.watch({}, handler);
240 } else {
241 compiler.run(handler);
242 // mimic watcher interface for API consistency
243 return {
244 close() {},
245 invalidate() {},
246 };
247 }
248 };
249
250 this.getMiddleware = () => {
251 const dev = webpackDevMiddleware(compiler);
252 const hot = webpackHotMiddleware(compiler, {log: false});
253 return (req, res, next) => {
254 dev(req, res, err => {
255 if (err) return next(err);
256 return hot(req, res, next);
257 });
258 };
259 };
260
261 this.clean = () => {
262 return new Promise((resolve, reject) => {
263 rimraf(`${dir}/.fusion`, e => (e ? reject(e) : resolve()));
264 });
265 };
266
267 return this;
268}
269
270function loadLegacyPkgConfig(dir) {
271 const appPkgJsonPath = path.join(dir, 'package.json');
272 const legacyPkgConfig = {};
273 if (fs.existsSync(appPkgJsonPath)) {
274 // $FlowFixMe
275 const appPkg = require(appPkgJsonPath);
276 if (typeof appPkg.node !== 'undefined') {
277 // eslint-disable-next-line no-console
278 console.warn(
279 [
280 `Warning: using a top-level "node" field in your app package.json to override node built-in shimming is deprecated.`,
281 `Please use the "nodeBuiltins" field in .fusionrc.js instead.`,
282 `See: https://github.com/fusionjs/fusion-cli/blob/master/docs/fusionrc.md#nodebuiltins`,
283 ].join(' ')
284 );
285 }
286 legacyPkgConfig.node = appPkg.node;
287 }
288 return legacyPkgConfig;
289}
290
291function createWorker(maxWorkers /* maxWorkers?: number */) {
292 if (require('os').cpus().length < 2) return void 0;
293 return new Worker(require.resolve('./loaders/babel-worker.js'), {
294 exposedMethods: ['runTransformation'],
295 forkOptions: {stdio: 'inherit'},
296 numWorkers: maxWorkers,
297 });
298}
299
300module.exports.Compiler = Compiler;