const { deepmerge, createDebug, winPath, resolve } = require('@umijs/utils'); const { ESBuildPlugin, ESBuildMinifyPlugin } = require('esbuild-loader'); const terserOptions = require('./terserOptions'); const fs = require('fs'); const path = require('path'); const webpack = require('webpack'); const { css, createCSSRule } = require('./css'); const Config = require('webpack-chain'); const pkg = require('../../package.json'); const ASSET_PATH = process.env.ASSET_PATH || '/'; const { ThemeColorReplacer, themePluginOption, } = require('../../config/plugin.config'); let defineConfig = require('../../config/config'); const resolveDefine = require('./resolveDefine'); const { getPkgPath, shouldTransform } = require('./pkgMatch'); const { TYPE_ALL_EXCLUDE, isMatch, excludeToPkgs, es5ImcompatibleVersionsToPkg, } = require('./nodeModulesTransform'); const lodash = require('lodash'); const { getTargetsAndBrowsersList, getBabelPresetOpts, getBabelOpts, } = require('@umijs/bundler-utils'); const { join } = require('lodash'); function getUserLibDir({ library }) { if ( (pkg.dependencies && pkg.dependencies[library]) || (pkg.devDependencies && pkg.devDependencies[library]) || // egg project using `clientDependencies` in ali tnpm (pkg.clientDependencies && pkg.clientDependencies[library]) ) { return winPath( path.dirname( // 通过 resolve 往上找,可支持 lerna 仓库 // lerna 仓库如果用 yarn workspace 的依赖不一定在 node_modules,可能被提到根目录,并且没有 link resolve.sync(`${library}/package.json`, { basedir: process.cwd(), }), ), ); } return null; } const defaultConfig = { autoprefixer: { flexbox: 'no-2009', }, cssnano: { mergeRules: false, minifyFontValues: { removeQuotes: false } }, nodeModulesTransform: { type: 'all', exclude: [], }, targets: { node: true, chrome: 49, firefox: 64, safari: 10, edge: 13, ios: 10, ie: 11, }, }; module.exports = options => { const chainConfig = new Config(); chainConfig.mode(options.mode); defineConfig = Object.assign(defineConfig, defaultConfig); const env = process.env; const isDev = process.env.NODE_ENV === 'development'; const isProd = process.env.NODE_ENV === 'production'; const disableCompress = process.env.COMPRESS === 'none'; const isWebpack5 = webpack.version.startsWith('5'); const devtool = options.devtool || false; chainConfig.devtool( isDev ? devtool === false ? false : devtool || 'cheap-module-source-map' : devtool, ); const useHash = defineConfig.hash && isProd; const absOutputPath = process.env.npm_config_releasepath ? path.resolve( process.env.npm_config_releasepath, pkg.name.toLocaleLowerCase(), ) : path.resolve(process.cwd(), pkg.name.toLocaleLowerCase()); chainConfig.output .path(absOutputPath) .filename(useHash ? `[name].[contenthash:8].js` : `[name].js`) .chunkFilename( useHash ? `[name].[contenthash:8].async.js` : `[name].chunk.js`, ) .publicPath(`/${pkg.name.toLocaleLowerCase()}/`) .futureEmitAssets(true) .crossOriginLoading('anonymous') .pathinfo(isDev || disableCompress); chainConfig.resolve.modules .add('node_modules') .add('src') .end() .extensions.merge(['.js', '.jsx', '.react.js']) .end() .mainFields.merge(['browser', 'jsnext:main', 'main']); const libraries = [ { name: 'react', path: path.dirname(require.resolve(`react/package.json`)), }, { name: 'react-dom', path: path.dirname(require.resolve(`react-dom/package.json`)), }, ]; libraries.forEach(library => { chainConfig.resolve.alias.set( library.name, getUserLibDir({ library: library.name }) || library.path, ); }); chainConfig.resolve.alias.set('@', path.join(__dirname, '../../', 'src')); if (defineConfig.alias) { Object.keys(defineConfig.alias).forEach(key => { chainConfig.resolve.alias.set(key, defineConfig.alias[key]); }); } chainConfig.target('web'); const { targets, browserslist } = getTargetsAndBrowsersList({ config: defineConfig, type: 'csr', }); let presetOpts = getBabelPresetOpts({ config: defineConfig, env: process.env.NODE_ENV, targets: targets, }); let preset = require('./babel-preset'); const getPreset = preset({ ...presetOpts, env: { useBuiltIns: 'entry', corejs: 3, modules: false, }, react: { development: isDev, }, reactRemovePropTypes: isProd, reactRequire: true, lockCoreJS3: {}, import: (presetOpts.import || []).concat([ { libraryName: 'antd', libraryDirectory: 'es', style: true }, { libraryName: 'antd-mobile', libraryDirectory: 'es', style: true }, ]), }); let babelOpts = { sourceType: 'unambiguous', babelrc: false, presets: [...getPreset.presets, ...(defineConfig.extraBabelPresets || [])], plugins: [ ...getPreset.plugins, ...(defineConfig.extraBabelPlugins || []), ].filter(Boolean), env: getPreset.env, }; chainConfig.module .rule('js') .test(/\.(js|mjs|jsx|ts|tsx)$/) .exclude.add(/node_modules/) .end() .use('babel-loader') .loader(require.resolve('babel-loader')) .options(babelOpts); if (defineConfig.extraBabelIncludes) { defineConfig.extraBabelIncludes.forEach((include, index) => { const rule = `extraBabelInclude_${index}`; chainConfig.module .rule(rule) .test(/\.(js|mjs|jsx)$/) .include.add(a => { if (path.isAbsolute(include)) { return path.isAbsolute(include); } if (!a.includes('node_modules')) return false; const pkgPath = getPkgPath(a); return shouldTransform(pkgPath, include); }) .end() .use('babel-loader') .loader(require.resolve('babel-loader')) .options(babelOpts); }); } chainConfig.module .rule('images') .test(/\.(png|jpe?g|gif|webp|ico)(\?.*)?$/) .use('url-loader') .loader(require.resolve('url-loader')) .options({ limit: defineConfig.inlineLimit || 10000, name: 'static/[name].[hash:8].[ext]', // require 图片的时候不用加 .default esModule: false, fallback: { loader: require.resolve('file-loader'), options: { name: 'static/[name].[hash:8].[ext]', esModule: false, }, }, }); chainConfig.module .rule('svg') .test(/\.(svg)(\?.*)?$/) .use('file-loader') .loader(require.resolve('@umijs/deps/compiled/file-loader')) .options({ name: 'static/[name].[hash:8].[ext]', esModule: false, }) .end(); chainConfig.module .rule('fonts') .test(/\.(eot|woff|woff2|ttf)(\?.*)?$/) .use('file-loader') .loader(require.resolve('file-loader')) .options({ name: 'static/[name].[hash:8].[ext]', esModule: false, }); chainConfig.module .rule('mdeia') .test(/\.(mp4|webm)$/) .use('url-loader') .loader(require.resolve('url-loader')) .options({ limit: 10000, outputPath: 'static', }); chainConfig.module .rule('plaintext') .test(/\.(txt|text|md)$/) .use('raw-loader') .loader(require.resolve('raw-loader')); css({ type: 'csr', config: defineConfig, webpackConfig: chainConfig, isDev, disableCompress, browserslist, miniCSSExtractPluginPath: '', miniCSSExtractPluginLoaderPath: '', }); if (defineConfig.externals) { chainConfig.externals(defineConfig.externals); } chainConfig.node.merge({ setImmediate: false, module: 'empty', dns: 'mock', http2: 'empty', process: 'mock', dgram: 'empty', fs: 'empty', net: 'empty', tls: 'empty', child_process: 'empty', ...options.node, }); if (defineConfig.ignoreMomentLocale) { chainConfig.plugin('ignore-moment-locale').use(webpack.IgnorePlugin, [ { resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/, }, ]); } chainConfig.plugin('define').use(webpack.DefinePlugin, [ resolveDefine({ define: Object.assign(defineConfig.define || {}, { 'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH), 'process.env.MOCK': JSON.stringify(true), paths: { cwd: process.cwd(), absNodeModulesPath: path.join(process.cwd(), 'node_modules'), absSrcPath: path.join(process.cwd(), 'src'), absPagesPath: path.join(process.cwd(), 'src/pages'), }, }), }), ]); const baseHtmlOptions = { inject: true, template: 'src/index.ejs', favicon: defineConfig.favicon ? defineConfig.favicon : false, title: defineConfig.title && defineConfig.title !== false ? defineConfig.title : '', chunks: defaultConfig.chunks ? defaultConfig.chunks : 'all', }; const htmlPluginOptions = isDev ? { ...babelOpts, } : { ...baseHtmlOptions, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, }; chainConfig .plugin('htmlPlugins') .use(require.resolve('html-webpack-plugin'), [htmlPluginOptions]); chainConfig .plugin('updateHtmlParams') .use(require.resolve('./plugins/HtmlGenerator'), [ { config: defineConfig, }, ]); chainConfig .plugin('replaceTheme') .use(ThemeColorReplacer, [themePluginOption]); const cwd = process.cwd(); const copyPatterns = [ fs.existsSync(path.join(process.cwd(), 'public')) && { from: path.join(cwd, 'public'), to: path.resolve( process.env.npm_config_releasepath || process.cwd(), pkg.name.toLocaleLowerCase(), ), }, ...(defineConfig.copy ? defineConfig.copy.map(item => { if (typeof item === 'string') { return { from: path.join(cwd, item), to: path.resolve( process.env.npm_config_releasepath || process.cwd(), pkg.name.toLocaleLowerCase(), ), }; } return { from: path.join(cwd, item.from), to: path.resolve( process.env.npm_config_releasepath || process.cwd(), pkg.name.toLocaleLowerCase() + '/' + item.to, ), }; }) : []), ].filter(Boolean); if (copyPatterns.length) { chainConfig .plugin('copy') .use(require.resolve('copy-webpack-plugin'), [copyPatterns]); } if (isDev && defineConfig.fastRefresh) { const debug = createDebug('civ:preset-build-in:fastRefresh'); chainConfig .plugin('fastRefresh') .after('hmr') .use(require('@pmmmwh/react-refresh-webpack-plugin'), [ { overlay: false }, ]); debug('FastRefresh webpack loaded'); } if (chainConfig.extraBabelIncludes) { chainConfig.extraBabelIncludes.forEach((include, index) => { const rule = `extraBabelInclude_${index}`; // prettier-ignore chainConfig.module .rule(rule) .test(/\.(js|mjs|jsx)$/) .include .add((a) => { // 支持绝对路径匹配 if (path.isAbsolute(include)) { return path.isAbsolute(include); } // 支持 node_modules 下的 npm 包 if (!a.includes('node_modules')) return false; const pkgPath = getPkgPath(a); return shouldTransform(pkgPath, include); }) .end() .use('babel-loader') .loader(require.resolve('babel-loader')) .options(babelOpts); }); } chainConfig.module .rule('ts-in-node_modules') .test(/\.(jsx|ts|tsx)$/) .include.add(/node_modules/) .end() .use('babel-loader') .loader(require.resolve('babel-loader')) .options(babelOpts); const rule = chainConfig.module .rule('js-in-node_modules') .test(/\.(js|mjs)$/); const nodeModulesTransform = defineConfig.nodeModulesTransform || { type: 'all', exclude: [], }; if (nodeModulesTransform.type === 'all') { const exclude = lodash.uniq([ ...TYPE_ALL_EXCLUDE, ...(nodeModulesTransform.exclude || []), ]); const pkgs = excludeToPkgs({ exclude }); rule.include .add(/node_modules/) .end() .exclude.add(path => { return isMatch({ path, pkgs }); }) .end(); } else { const pkgs = { ...es5ImcompatibleVersionsToPkg(), ...excludeToPkgs({ exclude: nodeModulesTransform.exclude || [] }), }; rule.include .add(path => { return isMatch({ path, pkgs, }); }) .end(); } rule .use('babel-loader') .loader(require.resolve('babel-loader')) .options({}); if ((isProd && defineConfig.analyze) || process.env.ANALYZE) { const mergeAnalyze = Object.assign( { analyzerMode: process.env.ANALYZE_MODE || 'server', analyzerPort: process.env.ANALYZE_PORT || 8888, openAnalyzer: process.env.ANALYZE_OPEN !== 'none', generateStatsFile: !!process.env.ANALYZE_DUMP, statsFilename: process.env.ANALYZE_DUMP || 'stats.json', logLevel: process.env.ANALYZE_LOG_LEVEL || 'info', defaultSizes: 'parsed', // stat // gzip }, defineConfig.analyze, ); chainConfig .plugin('bundle-analyzer') .use(require('umi-webpack-bundle-analyzer').BundleAnalyzerPlugin, [ mergeAnalyze || {}, ]); } if (process.env.PROGRESS !== 'none') { chainConfig.plugin('progress').use(require.resolve('webpackbar')); } if (process.env.FRIENDLY_ERROR !== 'none') { chainConfig .plugin('friendly-error') .use(require.resolve('friendly-errors-webpack-plugin'), [ { clearConsole: false, }, ]); } if (process.env.WEBPACK_PROFILE) { chainConfig.profile(true); const statsInclude = ['verbose', 'normal', 'minimal']; chainConfig.stats( statsInclude.includes(process.env.WEBPACK_PROFILE) ? process.env.WEBPACK_PROFILE : 'verbose', ); const StatsPlugin = require('stats-webpack-plugin'); chainConfig.plugin('stats-webpack-plugin').use( new StatsPlugin('stats.json', { chunkModules: true, }), ); } chainConfig.when( isDev, chainConfig => { if (isDev) { chainConfig.plugin('hmr').use(webpack.HotModuleReplacementPlugin); } }, chainConfig => { chainConfig.optimization.noEmitOnErrors(true); chainConfig.performance.hints(false); if (!isWebpack5) { chainConfig .plugin('hash-module-ids') .use(webpack.HashedModuleIdsPlugin, []); } if (disableCompress) { chainConfig.optimization.minimize(false); } else if (!options.__disableTerserForTest) { chainConfig.optimization .minimizer('terser') .use(require.resolve('terser-webpack-plugin'), [ { terserOptions: deepmerge( terserOptions, defineConfig.terserOptions || {}, ), sourceMap: defineConfig.devtool !== false, cache: process.env.TERSER_CACHE !== 'none', parallel: true, extractComments: false, }, ]); } if(defineConfig.esbuild) { const { target = 'es2015' } = defineConfig.esbuild || {}; const optsMap = { ['csr']: { minify: true, target } }; const opt = optsMap['csr']; chainConfig.optimization.minimize = true; chainConfig.optimization.minimizer = [new ESBuildMinifyPlugin(opt)]; chainConfig.plugin('es-build').use(new ESBuildPlugin()) } }, ); function createCSSRuleFn(opts) { createCSSRule({ webpackConfig: chainConfig, config: defineConfig, isDev, type: 'csr', browserslist, miniCSSExtractPluginLoaderPath: '', ...opts, }); } if (defineConfig.chainWebpack) { defineConfig.chainWebpack(chainConfig, { type: 'csr', env: process.env, webpack, createCSSRule: createCSSRuleFn, }); } // if (defineConfig.chunks) { // chainConfig // .plugin('htmlPlugins') // .use(require('html-webpack-plugin')) // .tap(options => { // options.chunks = defineConfig.chunks; // return options; // }); // } let ret = chainConfig.toConfig(); // speed-measure-webpack-plugin if (process.env.SPEED_MEASURE) { const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); const smpOption = process.env.SPEED_MEASURE === 'CONSOLE' ? { outputFormat: 'human', outputTarget: console.log } : { outputFormat: 'json', outputTarget: join(process.cwd(), 'speed-measure.json'), }; const smp = new SpeedMeasurePlugin(smpOption); ret = smp.wrap(ret); } let entry = options.entry; if (defineConfig.runtimePublicPath) { entry.push(require.resolve('./runtimePublicPathEntry')); } ret = { ...ret, // mode: options.mode, entry: entry, plugins: options.plugins.concat([...ret.plugins]), optimization: { ...options.optimization, ...ret.optimization, }, performance: options.performance || {}, }; return ret; };