UNPKG

nwb

Version:

A toolkit for React, Preact & Inferno apps, React libraries and other npm modules for the web, with no configuration (until you need it)

744 lines (622 loc) 23.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.mergeRuleConfig = mergeRuleConfig; exports.mergeLoaderConfig = mergeLoaderConfig; exports.createRuleConfigFactory = createRuleConfigFactory; exports.createLoaderConfigFactory = createLoaderConfigFactory; exports.createStyleLoaders = createStyleLoaders; exports.createRules = createRules; exports.createExtraRules = createExtraRules; exports.createPlugins = createPlugins; exports.getCompatConfig = getCompatConfig; exports.default = createWebpackConfig; exports.COMPAT_CONFIGS = exports.loaderConfigName = void 0; var _path = _interopRequireDefault(require("path")); var _autoprefixer = _interopRequireDefault(require("autoprefixer")); var _caseSensitivePathsWebpackPlugin = _interopRequireDefault(require("case-sensitive-paths-webpack-plugin")); var _copyWebpackPlugin = _interopRequireDefault(require("copy-webpack-plugin")); var _htmlWebpackPlugin = _interopRequireDefault(require("html-webpack-plugin")); var _miniCssExtractPlugin = _interopRequireDefault(require("mini-css-extract-plugin")); var _npmInstallWebpackPlugin = _interopRequireDefault(require("@insin/npm-install-webpack-plugin")); var _reactRefreshWebpackPlugin = _interopRequireDefault(require("@pmmmwh/react-refresh-webpack-plugin")); var _webpack = _interopRequireDefault(require("webpack")); var _webpackMerge = _interopRequireDefault(require("webpack-merge")); var _createBabelConfig = _interopRequireDefault(require("./createBabelConfig")); var _constants = require("./constants"); var _debug = _interopRequireDefault(require("./debug")); var _errors = require("./errors"); var _utils = require("./utils"); var _WebpackStatusPlugin = _interopRequireDefault(require("./WebpackStatusPlugin")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const DEFAULT_TERSER_CONFIG = { extractComments: false }; function createTerserConfig(userWebpackConfig) { if (userWebpackConfig.debug) { return (0, _webpackMerge.default)(DEFAULT_TERSER_CONFIG, { terserOptions: { output: { beautify: true }, mangle: false } }, // Preserve user 'compress' config if present, as it affects what gets // removed from the production build. typeof userWebpackConfig.terser === 'object' && typeof userWebpackConfig.terser.terserConfig === 'object' && 'compress' in userWebpackConfig.terser.terserConfig ? { terserOptions: { compress: userWebpackConfig.terser.terserConfig.compress } } : {}); } return (0, _webpackMerge.default)(DEFAULT_TERSER_CONFIG, typeof userWebpackConfig.terser === 'object' ? userWebpackConfig.terser : {}); } /** * Merge webpack rule config objects. */ function mergeRuleConfig(defaultConfig, buildConfig = {}, userConfig = {}) { let rule; // Omit the default loader and options if the user is configuring their own if (defaultConfig.loader && (userConfig.loader || userConfig.use)) { let { loader: defaultLoader, options: defaultOptions, // eslint-disable-line no-unused-vars ...defaultRuleConfig } = defaultConfig; rule = (0, _webpackMerge.default)(defaultRuleConfig, userConfig); } else { rule = (0, _utils.replaceArrayMerge)(defaultConfig, buildConfig, userConfig); } if (rule.options && Object.keys(rule.options).length === 0) { delete rule.options; } return rule; } /** * Merge webpack loader config objects. */ function mergeLoaderConfig(defaultConfig, buildConfig = {}, userConfig = {}) { let loader; // If the loader is being changed, only use the provided config if (userConfig.loader) { loader = { ...userConfig }; } else { // The only arrays used in default options are for PostCSS plugins, which we // want the user to be able to completely override. loader = (0, _utils.replaceArrayMerge)(defaultConfig, buildConfig, userConfig); } if (loader.options && Object.keys(loader.options).length === 0) { delete loader.options; } return loader; } /** * Create a function which configures a rule identified by a unique id, with * the option to override defaults with build-specific and user config. */ function createRuleConfigFactory(buildConfig = {}, userConfig = {}) { return function (id, defaultConfig) { if (id) { // Allow the user to turn off rules by configuring them with false if (userConfig[id] === false) { return null; } let rule = mergeRuleConfig(defaultConfig, buildConfig[id], userConfig[id]); return rule; } return defaultConfig; }; } /** * Create a function which configures a loader identified by a unique id, with * the option to override defaults with build-specific and user config. */ function createLoaderConfigFactory(buildConfig = {}, userConfig = {}) { return function (id, defaultConfig) { if (id) { let loader = mergeLoaderConfig(defaultConfig, buildConfig[id], userConfig[id]); return loader; } return defaultConfig; }; } /** * Create a function which applies a prefix to a name when a prefix is given, * unless the prefix ends with the name, in which case the prefix itself is * returned. * The latter rule is to allow rules created for CSS preprocessor plugins to * be given unique ids for user configuration without duplicating the name of * the rule. * e.g.: loaderConfigName('sass')('css') => 'sass-css' * loaderConfigName('sass')('sass') => 'sass' (as opposed to 'sass-sass') */ let loaderConfigName = prefix => name => { if (prefix && prefix.endsWith(name)) { return prefix; } return prefix ? `${prefix}-${name}` : name; }; /** * Create a list of chained loader config objects for a static build (default) * or serving. */ exports.loaderConfigName = loaderConfigName; function createStyleLoaders(createLoader, userWebpackConfig, options = {}) { let { preprocessor = null, prefix = null, server = false } = options; let name = loaderConfigName(prefix); let styleLoader = createLoader(name('style'), { loader: require.resolve('style-loader') }); let loaders = [createLoader(name('css'), { loader: require.resolve('css-loader'), options: { // Apply postcss-loader to @imports importLoaders: 1 } }), createLoader(name('postcss'), { loader: require.resolve('postcss-loader'), options: { ident: name('postcss'), plugins: createDefaultPostCSSPlugins(userWebpackConfig) } })]; if (preprocessor) { loaders.push(createLoader(preprocessor.id ? name(preprocessor.id) : null, preprocessor.config)); } if (server || userWebpackConfig.extractCSS === false) { loaders.unshift(styleLoader); return loaders; } else { loaders.unshift(createLoader(name('extract-css'), { loader: _miniCssExtractPlugin.default.loader })); return loaders; } } /** * Create style rules. By default, creates a single rule for .css files and for * any style preprocessor plugins present. The user can configure this to create * multiple rules if needed. */ function createStyleRules(server, userWebpackConfig, pluginConfig, createRule, createLoader) { let styleConfig = userWebpackConfig.styles || {}; let styleRules = []; // Configured styles rules, with individual loader configuration as part of // the definition. Object.keys(styleConfig).forEach(type => { let test, preprocessor; if (type === 'css') { test = /\.css$/; } else { let preprocessorConfig = pluginConfig.cssPreprocessors[type]; test = preprocessorConfig.test; preprocessor = { id: null, config: { loader: preprocessorConfig.loader } }; } let ruleConfigs = [].concat(...styleConfig[type]); ruleConfigs.forEach(ruleConfig => { let { loaders: loaderConfig, ...topLevelRuleConfig } = ruleConfig; // Empty build config, as all loader config for custom style rules will be // provided by the user. let styleRuleLoader = createLoaderConfigFactory({}, loaderConfig); styleRules.push({ test, use: createStyleLoaders(styleRuleLoader, userWebpackConfig, { preprocessor, server }), ...topLevelRuleConfig }); }); }); // Default CSS rule when nothing is configured, tweakable via webpack.rules by // unique id. if (!('css' in styleConfig)) { styleRules.push(createRule('css-rule', { test: /\.css$/, use: createStyleLoaders(createLoader, userWebpackConfig, { server }) })); } // Default rule for each CSS preprocessor plugin when nothing is configured, // tweakable via webpack.rules by unique id. if (pluginConfig.cssPreprocessors) { Object.keys(pluginConfig.cssPreprocessors).forEach(id => { if (id in styleConfig) return; let { test, loader: preprocessorLoader } = pluginConfig.cssPreprocessors[id]; styleRules.push(createRule(`${id}-rule`, { test, use: createStyleLoaders(createLoader, userWebpackConfig, { prefix: id, preprocessor: { id, config: { loader: preprocessorLoader } }, server }) })); }); } return styleRules; } /** * Final webpack rules config consists of: * - the default set of rules created in this function, with build and user * config tweaks based on rule id. * - extra rules defined in build config, with user config tweaks based * on rule id. * - extra rules created for CSS preprocessor plugins, with user config * tweaks based on loader id. * - extra rules defined in user config. */ function createRules(server, buildConfig = {}, userWebpackConfig = {}, pluginConfig = {}) { let createRule = createRuleConfigFactory(buildConfig, userWebpackConfig.rules); let createLoader = createLoaderConfigFactory(buildConfig, userWebpackConfig.rules); // Default options for url-loader let urlLoaderOptions = { // Don't inline anything by default limit: 1, // Always use a hash to prevent files with the same name causing issues name: '[name].[hash:8].[ext]' }; let rules = [createRule('babel', { test: /\.js$/, loader: require.resolve('babel-loader'), exclude: /node_modules[\\/](?!react-app-polyfill)/, options: { // Don't look for .babelrc files babelrc: false, // Cache transformations to the filesystem (in default temp dir) cacheDirectory: true } }), createRule('graphics', { test: /\.(gif|png|webp)$/, loader: require.resolve('url-loader'), options: { ...urlLoaderOptions } }), createRule('svg', { test: /\.svg$/, loader: require.resolve('url-loader'), options: { ...urlLoaderOptions } }), createRule('jpeg', { test: /\.jpe?g$/, loader: require.resolve('url-loader'), options: { ...urlLoaderOptions } }), createRule('fonts', { test: /\.(eot|otf|ttf|woff|woff2)$/, loader: require.resolve('url-loader'), options: { ...urlLoaderOptions } }), createRule('video', { test: /\.(mp4|ogg|webm)$/, loader: require.resolve('url-loader'), options: { ...urlLoaderOptions } }), createRule('audio', { test: /\.(wav|mp3|m4a|aac|oga)$/, loader: require.resolve('url-loader'), options: { ...urlLoaderOptions } }), // Extra rules from build config, still configurable via user config when // the rules specify an id. ...createExtraRules(buildConfig.extra, userWebpackConfig.rules)]; // Add rules with chained style loaders, using MiniCssExtractPlugin for builds if (userWebpackConfig.styles !== false) { rules = rules.concat(createStyleRules(server, userWebpackConfig, pluginConfig, createRule, createLoader)); } return rules.filter(Boolean); } /** * Create rules from rule definitions which may include an id attribute for * user customisation. It's assumed these are being created from build config. */ function createExtraRules(extraRules = [], userConfig = {}) { let createRule = createRuleConfigFactory({}, userConfig); return extraRules.map(extraRule => { let { id, ...ruleConfig } = extraRule; return createRule(id, ruleConfig); }); } /** * Plugin for HtmlPlugin which inlines the Webpack runtime code and chunk * manifest into its <script> tag. */ function inlineRuntimePlugin() { this.hooks.compilation.tap('inlineRuntimePlugin', compilation => { _htmlWebpackPlugin.default.getHooks(compilation).alterAssetTags.tapAsync('inlineRuntimePlugin', (data, cb) => { Object.keys(compilation.assets).forEach(assetName => { if (!/^runtime\.[a-z\d]+\.js$/.test(assetName)) return; let { children } = compilation.assets[assetName]; if (children && children[0]) { let tag = data.assetTags.scripts.find(tag => tag.attributes.src.endsWith(assetName)); delete tag.attributes.src; tag.innerHTML = children[0]._value; } }); cb(null, data); }); }); } function getCopyPluginArgs(buildConfig, userConfig) { let patterns = []; let options = {}; if (buildConfig) { patterns = patterns.concat(buildConfig); } if (userConfig) { patterns = patterns.concat(userConfig.patterns || []); options = userConfig.options || {}; } return [patterns, options]; } /** * Final webpack plugin config consists of: * - the default set of plugins created by this function based on whether or not * a server build is being configured, whether or not the build is for an * app (for which HTML will be generated), plus environment variables. * - extra plugins managed by this function, whose inclusion is triggered by * build config, which provides default configuration for them which can be * tweaked by user plugin config when appropriate. * - any extra plugins defined in build and user config (extra user plugins are * not handled here, but by the final merge of webpack.extra config). */ function createPlugins(server, buildConfig = {}, userConfig = {}) { let production = process.env.NODE_ENV === 'production'; let optimization = {}; let plugins = [// Enforce case-sensitive import paths new _caseSensitivePathsWebpackPlugin.default(), // Replace specified expressions with values new _webpack.default.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), ...buildConfig.define, ...userConfig.define }), // XXX Workaround until loaders migrate away from using this.options new _webpack.default.LoaderOptionsPlugin({ options: { context: process.cwd() } })]; if (server) { // HMR is enabled by default but can be explicitly disabled if (server.hot !== false) { plugins.push(new _webpack.default.HotModuleReplacementPlugin()); optimization.noEmitOnErrors = true; } if (buildConfig.reactRefresh) { plugins.push(new _reactRefreshWebpackPlugin.default()); } if (buildConfig.status) { plugins.push(new _WebpackStatusPlugin.default(buildConfig.status)); } } // If we're not serving, we're creating a static build else { if (userConfig.extractCSS !== false) { // Extract imported stylesheets out into .css files plugins.push(new _miniCssExtractPlugin.default({ filename: production ? `[name].[contenthash:8].css` : '[name].css', ...userConfig.extractCSS })); } // Move modules imported from node_modules/ into a vendor chunk when enabled if (buildConfig.vendor) { optimization.splitChunks = { // Split the entry chunk too chunks: 'all', // A 'vendors' cacheGroup will get defaulted if it doesn't exist, so // we override it to explicitly set the chunk name. cacheGroups: { vendors: { name: 'vendor', priority: -10, test: /[\\/]node_modules[\\/]/ } } }; } } if (production) { plugins.push(new _webpack.default.LoaderOptionsPlugin({ debug: false, minimize: true })); optimization.minimize = buildConfig.terser !== false && userConfig.terser !== false; if (buildConfig.terser !== false && userConfig.terser !== false) { optimization.minimizer = [compiler => { let TerserPlugin = require('terser-webpack-plugin'); let OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); new TerserPlugin(createTerserConfig(userConfig)).apply(compiler); new OptimizeCSSAssetsPlugin({ cssProcessorOptions: { map: { inline: false, annotation: true } } }).apply(compiler); }]; } } // Generate an HTML file for web apps which pulls in generated resources if (buildConfig.html) { plugins.push(new _htmlWebpackPlugin.default({ template: _path.default.join(__dirname, '../templates/webpack-template.html'), ...buildConfig.html, ...userConfig.html })); // Extract the Webpack runtime and manifest into its own chunk // The default runtime chunk name is 'runtime' with this configuration optimization.runtimeChunk = 'single'; // Inline the runtime and manifest plugins.push(inlineRuntimePlugin); } // Copy static resources if (buildConfig.copy || userConfig.copy) { const [patterns, options] = getCopyPluginArgs(buildConfig.copy, userConfig.copy); if (patterns.length > 0) { plugins.push(new _copyWebpackPlugin.default({ patterns, options })); } } // Automatically install missing npm dependencies and add them to package.json // if present. // Must be enabled with an --install or --auto-install flag. if (buildConfig.autoInstall) { plugins.push(new _npmInstallWebpackPlugin.default({ peerDependencies: false, quiet: true, ...userConfig.install })); } // Insert a banner comment at the top of generated files - used for UMD builds if (buildConfig.banner) { plugins.push(new _webpack.default.BannerPlugin({ banner: buildConfig.banner })); } // Escape hatch for any extra plugins a particular build ever needs to add if (buildConfig.extra) { plugins = plugins.concat(buildConfig.extra); } return { optimization, plugins }; } function createDefaultPostCSSPlugins(userWebpackConfig) { // Users can override browser versions for Autoprefixer using `browsers` or // `webpack.autoprefixer.overrideBrowserslist` config. let overrideBrowserslist = process.env.NODE_ENV === 'production' ? userWebpackConfig.browsers && userWebpackConfig.browsers.production || _constants.DEFAULT_BROWSERS_PROD : userWebpackConfig.browsers && userWebpackConfig.browsers.development || _constants.DEFAULT_BROWSERS_DEV; return [(0, _autoprefixer.default)({ overrideBrowserslist, ...userWebpackConfig.autoprefixer })]; } const COMPAT_CONFIGS = { intl(options) { return { plugins: [new _webpack.default.ContextReplacementPlugin(/intl[/\\]locale-data[/\\]jsonp$/, new RegExp(`^\\.\\/(${options.locales.join('|')})$`))] }; }, moment(options) { return { plugins: [new _webpack.default.ContextReplacementPlugin(/moment[/\\]locale$/, new RegExp(`^\\.\\/(${options.locales.join('|')})$`))] }; }, 'react-intl'(options) { return { plugins: [new _webpack.default.ContextReplacementPlugin(/react-intl[/\\]locale-data$/, new RegExp(`^\\.\\/(${options.locales.join('|')})$`))] }; } }; /** * Create a chunk of webpack config containing compatibility tweaks for * libraries which are known to cause issues, to be merged into the generated * config. * Returns null if there's nothing to merge based on user config. */ exports.COMPAT_CONFIGS = COMPAT_CONFIGS; function getCompatConfig(userCompatConfig = {}) { let configs = []; Object.keys(userCompatConfig).map(lib => { if (!userCompatConfig[lib]) return; let compatConfig = COMPAT_CONFIGS[lib]; if ((0, _utils.typeOf)(compatConfig) === 'function') { compatConfig = compatConfig(userCompatConfig[lib]); if (!compatConfig) return; } configs.push(compatConfig); }); if (configs.length === 0) return null; if (configs.length === 1) return { ...configs[0] }; return (0, _webpackMerge.default)(...configs); } /** * Create a webpack config with a curated set of default rules suitable for * creating a static build (default) or serving an app with hot reloading. */ function createWebpackConfig(buildConfig, pluginConfig = {}, userConfig = {}) { (0, _debug.default)('createWebpackConfig buildConfig: %s', (0, _utils.deepToString)(buildConfig)); // Final webpack config is primarily driven by build configuration for the nwb // command being run. Each command configures a default, working webpack // configuration for the task it needs to perform. let { // These build config items are used to create chunks of webpack config, // rather than being included as-is. babel: buildBabelConfig = {}, entry, output: buildOutputConfig, plugins: buildPluginConfig = {}, resolve: buildResolveConfig = {}, rules: buildRulesConfig = {}, server = false, // Any other build config provided is merged directly into the final webpack // config to provide the rest of the default config. ...otherBuildConfig } = buildConfig; let userWebpackConfig = userConfig.webpack || {}; let userOutputConfig = {}; if ('publicPath' in userWebpackConfig) { userOutputConfig.publicPath = userWebpackConfig.publicPath; } let userResolveConfig = {}; if (userWebpackConfig.aliases) { userResolveConfig.alias = userWebpackConfig.aliases; } // Generate config for babel-loader and set it as loader config for the build buildRulesConfig.babel = { options: (0, _createBabelConfig.default)(buildBabelConfig, userConfig.babel, userConfig.path, userConfig.browsers) }; let webpackConfig = { mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', module: { rules: createRules(server, buildRulesConfig, userWebpackConfig, pluginConfig), strictExportPresence: true }, output: { ...buildOutputConfig, ...userOutputConfig }, performance: { hints: false }, // Plugins are configured via a 'plugins' list and 'optimization' config ...createPlugins(server, buildPluginConfig, userWebpackConfig), resolve: (0, _webpackMerge.default)(buildResolveConfig, userResolveConfig), resolveLoader: { modules: ['node_modules', _path.default.join(__dirname, '../node_modules')] }, ...otherBuildConfig }; if (entry) { webpackConfig.entry = entry; } // Create and merge compatibility configuration into the generated config if // specified. if (userWebpackConfig.compat) { let compatConfig = getCompatConfig(userWebpackConfig.compat); if (compatConfig) { webpackConfig = (0, _webpackMerge.default)(webpackConfig, compatConfig); } } // Any extra user webpack config is merged into the generated config to give // them even more control. if (userWebpackConfig.extra) { webpackConfig = (0, _webpackMerge.default)(webpackConfig, userWebpackConfig.extra); } // Finally, give them a chance to do whatever they want with the generated // config. if ((0, _utils.typeOf)(userWebpackConfig.config) === 'function') { webpackConfig = userWebpackConfig.config(webpackConfig); if (!webpackConfig) { throw new _errors.UserError(`webpack.config() in ${userConfig.path} didn't return anything - it must return the Webpack config object.`); } } return webpackConfig; }