UNPKG

@enact/cli

Version:

Full-featured build environment tool for Enact applications.

339 lines (313 loc) 10.8 kB
/* eslint-env node, es6 */ // @remove-on-eject-begin /** * Portions of this source code file are from create-react-app, used under the * following MIT license: * * Copyright (c) 2013-present, Facebook, Inc. * https://github.com/facebook/create-react-app * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ // @remove-on-eject-end const path = require('path'); const {filesize} = require('filesize'); const fs = require('fs-extra'); const minimist = require('minimist'); const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); const printBuildError = require('react-dev-utils/printBuildError'); const webpack = require('webpack'); const {optionParser: app, mixins, configHelper: helper} = require('@enact/dev-utils'); let chalk; let stripAnsi; function displayHelp() { let e = 'node ' + path.relative(process.cwd(), __filename); if (require.main !== module) e = 'enact pack'; console.log(' Usage'); console.log(` ${e} [options]`); console.log(); console.log(' Options'); console.log(' -o, --output Specify an output directory'); console.log(' --content-hash Add a unique hash to output file names based on the content of an asset'); console.log(' -w, --watch Rebuild on file changes'); console.log(' -p, --production Build in production mode'); console.log(' -i, --isomorphic Use isomorphic code layout'); console.log(' (includes prerendering)'); console.log(' -l, --locales Locales for isomorphic mode; one of:'); console.log(' <comma-separated-values> Locale list'); console.log(' <JSON-filepath> - Read locales from JSON file'); console.log(' "none" - Disable locale-specific handling'); console.log(' "used" - Detect locales used within ./resources/'); console.log(' "tv" - Locales supported on webOS TV'); console.log(' "signage" - Locales supported on webOS signage'); console.log(' "all" - All locales that iLib supports'); console.log(' -s, --snapshot Generate V8 snapshot blob'); console.log(' (requires V8_MKSNAPSHOT set)'); console.log(' -m, --meta JSON to override package.json enact metadata'); console.log(' -c, --custom-skin Build with a custom skin'); console.log(' --no-animation Build without effects such as animation and shadow'); console.log(' --stats Output bundle analysis file'); console.log(' --verbose Verbose log build details'); console.log(' -v, --version Display version information'); console.log(' -h, --help Display help information'); console.log(); /* Private Options: --entry Specify an override entrypoint --no-minify Will skip minification during production build --no-split-css Will not split CSS into separate files --framework Builds the @enact/*, react, and react-dom into an external framework --externals Specify a local directory path to the standalone external framework --externals-public Remote public path to the external framework for use injecting into HTML --externals-polyfill Flag whether to use external polyfill (or include in framework build) --ilib-additional-path Specify iLib additional resources path */ process.exit(0); } function details(err, stats, output) { let messages; if (err) { if (!err.message) return err; let msg = err.message; // Add additional information for postcss errors if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) { msg += '\nCompileError: Begins at CSS selector ' + err['postcssNode'].selector; } // Generate pretty/formatted warnins/errors messages = formatWebpackMessages({ errors: [msg], warnings: [] }); } else { // Remove any ESLint fixable notices since we're not running via eslint command // and don't support a `--fix` optiob ourselves; don't want to confuse devs stats.compilation.warnings.forEach(w => { const eslintFix = /\n.* potentially fixable with the `--fix` option./gm; w.message = w.message.replace(eslintFix, ''); }); // Generate pretty/formatted warnins/errors const statsJSON = stats.toJson({all: false, warnings: true, errors: true}); messages = formatWebpackMessages(statsJSON); } if (messages.errors.length) { return new Error(messages.errors.join('\n\n')); } else if ( typeof process.env.CI === 'string' && process.env.CI.toLowerCase() !== 'false' && messages.warnings.length ) { // Ignore sourcemap warnings in CI builds. See #8227 for more info. const filteredWarnings = messages.warnings.filter(w => !/Failed to parse source map/.test(w)); if (filteredWarnings.length) { console.log( chalk.yellow( '\nTreating warnings as errors because process.env.CI = true. \n' + 'Most CI servers set it automatically.\n' ) ); return new Error(filteredWarnings.join('\n\n')); } } else { copyPublicFolder(output); if (messages.warnings.length) { console.log(chalk.yellow('Compiled with warnings:\n')); console.log(messages.warnings.join('\n\n') + '\n'); } else { console.log(chalk.green('Compiled successfully.')); } if (process.env.NODE_ENV === 'development') { console.log( chalk.yellow( 'NOTICE: This build contains debugging functionality and may run' + ' slower than in production mode.' ) ); } console.log(); printFileSizes(stats, output); console.log(); } } function copyPublicFolder(output) { const staticAssets = './public'; if (fs.existsSync(staticAssets)) { fs.copySync(staticAssets, output, { dereference: true }); } } // Print a detailed summary of build files. function printFileSizes(stats, output) { const assets = stats .toJson({all: false, assets: true, cachedAssets: true}) .assets.filter(asset => /\.(js|css|bin)$/.test(asset.name)) .map(asset => { const size = fs.statSync(path.join(output, asset.name)).size; return { folder: path.relative(app.context, path.join(output, path.dirname(asset.name))), name: path.basename(asset.name), size: size, sizeLabel: filesize(size) }; }); assets.sort((a, b) => b.size - a.size); const longestSizeLabelLength = Math.max.apply( null, assets.map(a => stripAnsi(a.sizeLabel).length) ); assets.forEach(asset => { let sizeLabel = asset.sizeLabel; const sizeLength = stripAnsi(sizeLabel).length; if (sizeLength < longestSizeLabelLength) { const rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); sizeLabel += rightPadding; } console.log(' ' + sizeLabel + ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name)); }); } function printErrorDetails(err, handler) { console.log(); if (process.env.TSC_COMPILE_ON_ERROR === 'true') { console.log( chalk.yellow( 'Compiled with the following type errors (you may want to check ' + 'these before deploying your app):\n' ) ); printBuildError(err); } else { console.log(chalk.red('Failed to compile.\n')); printBuildError(err); if (handler) handler(); } } // Create the production build and print the deployment instructions. function build(config) { if (process.env.NODE_ENV === 'development') { console.log('Creating a development build...'); } else { console.log('Creating an optimized production build...'); } return new Promise((resolve, reject) => { const compiler = webpack(config); compiler.run((err, stats) => { err = details(err, stats, config.output.path); if (err) { reject(err); } else { resolve(); } }); }); } // Create the build and watch for changes. function watch(config) { // Make sure webpack doesn't immediate bail on errors when watching. config.bail = false; if (process.env.NODE_ENV === 'development') { console.log('Creating a development build and watching for changes...'); } else { console.log('Creating an optimized production build and watching for changes...'); } copyPublicFolder(config.output.path); webpack(config).watch({}, (err, stats) => { err = details(err, stats, config.output.path); if (err) { printErrorDetails(err); } console.log(); }); } function api(opts = {}) { if (opts.meta) { let meta = opts.meta; if (typeof meta === 'string') { try { meta = JSON.parse(opts.meta); } catch (e) { throw new Error('Invalid metadata; must be a valid JSON string.\n' + e.message); } } app.applyEnactMeta(meta); } if (opts['custom-skin']) { app.applyEnactMeta({template: path.join(__dirname, '..', 'config', 'custom-skin-template.ejs')}); } // make the framework option available globally in order to be used by the eslint-webpack-plugin custom configuration process.env.FRAMEWORK = opts.framework; // Do this as the first thing so that any code reading it knows the right env. const configFactory = require('../config/webpack.config'); const config = configFactory( opts.production ? 'production' : 'development', opts['content-hash'], opts.isomorphic, !opts.animation, !opts['split-css'], opts['ilib-additional-path'] ); // Set any entry path override if (opts.entry || app.entry) helper.replaceEntry(config, opts.entry || app.entry); // Set any output path override if (opts.output) config.output.path = path.resolve(opts.output); mixins.apply(config, opts); // Remove all content but keep the directory so that // if you're in it, you don't end up in Trash return fs.emptyDir(config.output.path).then(() => { // Start the webpack build if (opts.watch) { // This will run infinitely until killed, even through errors watch(config); } else { return build(config); } }); } function cli(args) { const opts = minimist(args, { boolean: [ 'content-hash', 'custom-skin', 'minify', 'split-css', 'framework', 'externals-corejs', 'stats', 'production', 'isomorphic', 'snapshot', 'animation', 'verbose', 'watch', 'help' ], string: ['externals', 'externals-public', 'locales', 'entry', 'ilib-additional-path', 'output', 'meta'], default: {minify: true, 'split-css': true, animation: true}, alias: { o: 'output', p: 'production', i: 'isomorphic', l: 'locales', s: 'snapshot', m: 'meta', c: 'custom-skin', w: 'watch', h: 'help' } }); if (opts.help) displayHelp(); process.chdir(app.context); import('chalk').then(({default: _chalk}) => { chalk = _chalk; import('strip-ansi').then(({default: _stripAnsi}) => { stripAnsi = _stripAnsi; api(opts).catch(err => { printErrorDetails(err, () => { process.exit(1); }); }); }); }); } module.exports = {api, cli}; if (require.main === module) cli(process.argv.slice(2));