fusion-cli/build/compiler.js (229 lines of code) (raw):

/** Copyright (c) 2018 Uber Technologies, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ /* eslint-env node */ const fs = require('fs'); const path = require('path'); const webpack = require('webpack'); const chalk = require('chalk'); const webpackHotMiddleware = require('webpack-hot-middleware'); const rimraf = require('rimraf'); const webpackDevMiddleware = require('../lib/simple-webpack-dev-middleware'); const {getWebpackConfig} = require('./get-webpack-config.js'); const { DeferredState, SyncState, MergedDeferredState, } = require('./shared-state-containers.js'); const mergeChunkMetadata = require('./merge-chunk-metadata'); const loadFusionRC = require('./load-fusionrc.js'); const Worker = require('jest-worker').default; function getErrors(info) { let errors = [].concat(info.errors); if (info.children.length) { errors = errors.concat( info.children.reduce((x, child) => { return x.concat(getErrors(child)); }, []) ); } return dedupeErrors(errors); } function getWarnings(info) { let warnings = [].concat(info.warnings); if (info.children.length) { warnings = warnings.concat( info.children.reduce((x, child) => { return x.concat(getWarnings(child)); }, []) ); } return dedupeErrors(warnings); } function dedupeErrors(items) { const re = /BabelLoaderError(.|\n)+( {4}at transpile)/gim; const set = new Set(items.map(item => item.replace(re, '$2'))); return Array.from(set); } function getStatsLogger({dir, logger, env}) { return (err, stats) => { // syntax errors are logged 4 times (once by webpack, once by babel, once on server and once on client) // we only want to log each syntax error once const isProd = env === 'production'; if (err) { logger.error(err.stack || err); if (err.details) { logger.error(err.details); } return; } const file = path.resolve(dir, '.fusion/stats.json'); const info = stats.toJson({context: path.resolve(dir)}); fs.writeFile(file, JSON.stringify(info, null, 2), () => {}); if (stats.hasErrors()) { getErrors(info).forEach(e => logger.error(e)); } // TODO(#13): These logs seem to be kinda noisy for dev. if (isProd) { info.children.forEach(child => { child.assets .slice() .filter(asset => { return !asset.name.endsWith('.map'); }) .sort((a, b) => { return b.size - a.size; }) .forEach(asset => { logger.info(`Entrypoint: ${chalk.bold(child.name)}`); logger.info(`Asset: ${chalk.bold(asset.name)}`); logger.info(`Size: ${chalk.bold(asset.size)} bytes`); }); }); } if (stats.hasWarnings()) { getWarnings(info).forEach(e => logger.warn(e)); } }; } /*:: type CompilerType = { on: (type: any, callback: any) => any, start: (callback: any) => any, getMiddleware: () => any, clean: () => any, }; */ /*:: type CompilerOpts = { serverless?: boolean, dir?: string, env: "production" | "development", hmr?: boolean, watch?: boolean, forceLegacyBuild?: boolean, logger?: any, preserveNames?: boolean, minify?: boolean, modernBuildOnly?: boolean, maxWorkers?: number, skipSourceMaps?: boolean, }; */ function Compiler( { dir = '.', env, hmr = true, forceLegacyBuild, preserveNames, watch = false, logger = console, minify = true, serverless = false, modernBuildOnly = false, skipSourceMaps = false, maxWorkers, } /*: CompilerOpts */ ) /*: CompilerType */ { const root = path.resolve(dir); const fusionConfig = loadFusionRC(root); const legacyPkgConfig = loadLegacyPkgConfig(root); const clientChunkMetadata = new DeferredState(); const legacyClientChunkMetadata = new DeferredState(); const legacyBuildEnabled = new SyncState( (forceLegacyBuild || !watch || env === 'production') && !(modernBuildOnly || fusionConfig.modernBuildOnly) ); const mergedClientChunkMetadata /*: any */ = new MergedDeferredState( [ {deferred: clientChunkMetadata, enabled: new SyncState(true)}, {deferred: legacyClientChunkMetadata, enabled: legacyBuildEnabled}, ], mergeChunkMetadata ); const state = { clientChunkMetadata, legacyClientChunkMetadata, mergedClientChunkMetadata, i18nManifest: new Map(), i18nDeferredManifest: new DeferredState(), legacyBuildEnabled, }; let worker = createWorker(maxWorkers); const sharedOpts = { dir: root, dev: env === 'development', hmr, watch, state, fusionConfig, legacyPkgConfig, skipSourceMaps, preserveNames, // TODO: Remove redundant zopfli option zopfli: fusionConfig.zopfli != undefined ? fusionConfig.zopfli : true, gzip: fusionConfig.gzip != undefined ? fusionConfig.gzip : true, brotli: fusionConfig.brotli != undefined ? fusionConfig.brotli : true, minify, worker, }; const compiler = webpack([ getWebpackConfig({id: 'client-modern', ...sharedOpts}), getWebpackConfig({ id: serverless ? 'serverless' : 'server', ...sharedOpts, }), ]); if (process.env.LOG_END_TIME == 'true') { compiler.hooks.done.tap('BenchmarkTimingPlugin', stats => { /* eslint-disable-next-line no-console */ console.log(`End time: ${Date.now()}`); }); } if (watch) { compiler.hooks.watchRun.tap('StartWorkersAgain', () => { if (worker === void 0) worker = createWorker(maxWorkers); }); compiler.hooks.watchClose.tap('KillWorkers', stats => { if (worker !== void 0) worker.end(); worker = void 0; }); } else compiler.hooks.done.tap('KillWorkers', stats => { if (worker !== void 0) worker.end(); worker = void 0; }); const statsLogger = getStatsLogger({dir, logger, env}); this.on = (type, callback) => compiler.hooks[type].tap('compiler', callback); this.start = cb => { cb = cb || function noop(err, stats) {}; // Handler may be called multiple times by `watch` // But only call `cb` the first time // subsequent rebuilds are subscribed to with 'compiler.on('done')' let hasCalledCb = false; const handler = (err, stats) => { statsLogger(err, stats); if (!hasCalledCb) { hasCalledCb = true; cb(err, stats); } }; if (watch) { return compiler.watch({}, handler); } else { compiler.run(handler); // mimic watcher interface for API consistency return { close() {}, invalidate() {}, }; } }; this.getMiddleware = () => { const dev = webpackDevMiddleware(compiler); const hot = webpackHotMiddleware(compiler, {log: false}); return (req, res, next) => { dev(req, res, err => { if (err) return next(err); return hot(req, res, next); }); }; }; this.clean = () => { return new Promise((resolve, reject) => { rimraf(`${dir}/.fusion`, e => (e ? reject(e) : resolve())); }); }; return this; } function loadLegacyPkgConfig(dir) { const appPkgJsonPath = path.join(dir, 'package.json'); const legacyPkgConfig = {}; if (fs.existsSync(appPkgJsonPath)) { // $FlowFixMe const appPkg = require(appPkgJsonPath); if (typeof appPkg.node !== 'undefined') { // eslint-disable-next-line no-console console.warn( [ `Warning: using a top-level "node" field in your app package.json to override node built-in shimming is deprecated.`, `Please use the "nodeBuiltins" field in .fusionrc.js instead.`, `See: https://github.com/fusionjs/fusion-cli/blob/master/docs/fusionrc.md#nodebuiltins`, ].join(' ') ); } legacyPkgConfig.node = appPkg.node; } return legacyPkgConfig; } function createWorker(maxWorkers /* maxWorkers?: number */) { if (require('os').cpus().length < 2) return void 0; return new Worker(require.resolve('./loaders/babel-worker.js'), { exposedMethods: ['runTransformation'], forkOptions: {stdio: 'inherit'}, numWorkers: maxWorkers, }); } module.exports.Compiler = Compiler;