UNPKG

fec-builder

Version:

通用的前端构建工具,屏蔽业务无关的细节配置,开箱即用

186 lines (166 loc) 6.24 kB
import { mapValues } from 'lodash' import fs from 'fs' import path from 'path' import { Configuration, DefinePlugin } from 'webpack' import HtmlPlugin from 'html-webpack-plugin' import CopyPlugin from 'copy-webpack-plugin' import ReactFastRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin' import CssMinimizerPlugin from 'css-minimizer-webpack-plugin' import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' import WebpackBarPlugin from 'webpackbar' import ImageMinimizerPlugin from 'image-minimizer-webpack-plugin' import { getBuildRoot, abs, getStaticPath, getDistPath, getSrcPath } from '../utils/paths' import { BuildConfig, findBuildConfig, getNeedAnalyze } from '../utils/build-conf' import { addTransforms, svgoConfig } from './transform' import { Env, getEnv } from '../utils/build-env' import logger from '../utils/logger' import { getPathFromUrl, getPageFilename } from '../utils' import { appendPlugins, processSourceMap, appendCacheGroups, parseOptimizationConfig } from '../utils/webpack' const dirnameOfBuilder = path.resolve(__dirname, '../..') const nodeModulesOfBuilder = path.resolve(dirnameOfBuilder, 'node_modules') /** 获取 webpack 配置(构建用) */ export async function getConfig(): Promise<Configuration> { const buildConfig = await findBuildConfig() let config: Configuration = { target: 'web', // TODO: 使用 `browserslist:...` 可能合适? 详情见 https://webpack.js.org/configuration/target/ mode: getMode(), context: getBuildRoot(), resolve: { // 同默认配置,这里写出来是因为后续会有新增 extensions extensions: ['.wasm', '.mjs', '.js', '.json'], modules: [ getSrcPath(buildConfig), 'node_modules', nodeModulesOfBuilder, abs('node_modules') ] }, resolveLoader: { modules: [ 'node_modules', nodeModulesOfBuilder ] }, entry: mapValues(buildConfig.entries, entryFile => abs(entryFile)), module: { rules: [] }, plugins: [], output: { path: getDistPath(buildConfig), filename: 'static/[name]-[contenthash].js', chunkFilename: 'static/[id]-[chunkhash].js', assetModuleFilename: 'static/[name]-[contenthash][ext]', publicPath: ( getEnv() === Env.Prod ? buildConfig.publicUrl : getPathFromUrl(buildConfig.publicUrl) ), environment: { // 这里控制 webpack 本身的运行时代码(而不是业务代码), // 对于语言 feature 先全部配合不支持,以确保 webpack 会产出兼容性最好的代码; // TODO: 后续考虑通过使用 build config 中的 targets.browsers 来挨个判断是否支持 arrowFunction: false, bigIntLiteral: false, const: false, destructuring: false, dynamicImport: false, forOf: false, module: false } }, optimization: { minimizer: [ '...', new CssMinimizerPlugin() ] } } let baseChunks: string[] = [] if (getEnv() === Env.Prod) { const result = parseOptimizationConfig(buildConfig.optimization) baseChunks = result.baseChunks config = appendCacheGroups(config, result.cacheGroups) } if (getEnv() === Env.Dev) { config = processSourceMap(config, buildConfig.optimization.highQualitySourceMap) } config = addTransforms(config, buildConfig) const htmlPlugins = Object.entries(buildConfig.pages).map(([ name, { template, entries } ]) => { return new HtmlPlugin({ template: abs(template), filename: getPageFilename(name), chunks: [...baseChunks, ...entries], chunksSortMode: 'manual' }) }) const definePlugin = new DefinePlugin( // webpack DefinePlugin 只是简单的文本替换,这里进行 JSON stringify 转换 mapValues({ 'process.env.NODE_ENV': getEnv(), ...buildConfig.envVariables }, JSON.stringify) ) const staticDirCopyPlugin = getStaticDirCopyPlugin(buildConfig) config = appendPlugins( config, ...htmlPlugins, definePlugin, staticDirCopyPlugin, new WebpackBarPlugin({ color: 'green' }) ) if (getEnv() === Env.Prod) { config = appendPlugins(config, new MiniCssExtractPlugin({ filename: 'static/[name]-[contenthash].css', chunkFilename: 'static/[id]-[chunkhash].css' })) } if (getNeedAnalyze()) { config = appendPlugins(config, new BundleAnalyzerPlugin()) } if (getEnv() === Env.Prod && buildConfig.optimization.compressImage) { config = appendPlugins(config, new ImageMinimizerPlugin({ minimizerOptions: { plugins: [ ['mozjpeg', { progressive: true, quality: 65 }], ['gifsicle', { interlaced: false }], ['svgo', svgoConfig] // 这里先不做 png 的压缩,因为 imagemin-pngquant 有可能会产生负优化(结果文件比源文件体积大), // 而且目前不支持 option 来在产生负优化时直接使用源文件,相关 issue https://github.com/kornelski/pngquant/issues/338 // ['pngquant', { quality: [0.65, 0.9], speed: 4 }], ] } })) } return config } /** 获取 webpack 配置(dev server 用,不含 dev server 配置) */ export async function getServeConfig() { const config = await getConfig() return appendPlugins( config, new ReactFastRefreshPlugin() ) } /** 获取合适的 webpack mode */ function getMode(): Configuration['mode'] { const buildEnv = getEnv() if (buildEnv === Env.Dev) return 'development' if (buildEnv === Env.Prod) return 'production' return 'none' } /** 构造用于 static 目录复制的 plugin 实例 */ function getStaticDirCopyPlugin(buildConfig: BuildConfig) { const staticPath = getStaticPath(buildConfig) if (!fs.existsSync(staticPath)) return null try { const stats = fs.statSync(staticPath) if (!stats.isDirectory()) { throw new Error('staticPath not a directory') } return new CopyPlugin({ patterns: [{ from: staticPath, to: 'static', toType: 'dir' }] }) } catch (e) { logger.warn('Copy staticDir content failed:', e.message) } }