ice/packages/webpack-config/src/index.ts

441 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as path from 'path';
import { createRequire } from 'module';
import crypto from 'crypto';
import fg from 'fast-glob';
import ReactRefreshWebpackPlugin from '@ice/bundles/compiled/@pmmmwh/react-refresh-webpack-plugin/lib/index.js';
import bundleAnalyzer from '@ice/bundles/compiled/webpack-bundle-analyzer/index.js';
import lodash from '@ice/bundles/compiled/lodash/index.js';
import CssMinimizerPlugin from '@ice/bundles/compiled/css-minimizer-webpack-plugin/index.js';
import TerserPlugin from '@ice/bundles/compiled/terser-webpack-plugin/index.js';
import ForkTsCheckerPlugin from '@ice/bundles/compiled/fork-ts-checker-webpack-plugin/index.js';
import ESlintPlugin from '@ice/bundles/compiled/eslint-webpack-plugin/index.js';
import CopyPlugin from '@ice/bundles/compiled/copy-webpack-plugin/index.js';
import type { NormalModule, Compiler, Configuration } from 'webpack';
import type webpack from 'webpack';
import { compilationPlugin, compileExcludes, getCompilerPlugins, getDefineVars, getAliasWithRoot, getDevtoolValue, getSupportedBrowsers } from '@ice/shared-config';
import type { Config, ModifyWebpackConfig } from '@ice/shared-config/types.js';
import configAssets from './config/assets.js';
import configCss from './config/css.js';
import AssetsManifestPlugin from './webpackPlugins/AssetsManifestPlugin.js';
import EnvReplacementPlugin from './webpackPlugins/EnvReplacementPlugin.js';
import getSplitChunksConfig, { FRAMEWORK_BUNDLES } from './config/splitChunks.js';
const require = createRequire(import.meta.url);
const { merge } = lodash;
const { BundleAnalyzerPlugin } = bundleAnalyzer;
const watchIgnoredRegexp = ['**/.git/**', '**/node_modules/**'];
interface GetWebpackConfigOptions {
rootDir: string;
config: Config;
webpack: typeof webpack;
runtimeTmpDir: string;
userConfigHash: string;
getExpandedEnvs: () => Record<string, string>;
runtimeDefineVars?: Record<string, any>;
getRoutesFile?: () => string[];
isServer?: boolean;
}
enum JSMinifier {
terser = 'terser',
swc = 'swc',
}
function getEntry(rootDir: string, runtimeTmpDir: string) {
// check entry.client.tsx
let entryFile = fg.sync('entry.client.{tsx,ts,jsx.js}', {
cwd: path.join(rootDir, 'src'),
absolute: true,
})[0];
if (!entryFile) {
// use generated file in template directory
entryFile = path.join(rootDir, runtimeTmpDir, 'entry.client.tsx');
}
// const dataLoaderFile = path.join(rootDir, '.ice/data-loader.ts');
return {
main: [entryFile],
// FIXME: https://github.com/ice-lab/ice-next/issues/217, https://github.com/ice-lab/ice-next/issues/199
// loader: [dataLoaderFile],
};
}
export function getWebpackConfig(options: GetWebpackConfigOptions): Configuration {
const {
rootDir,
config,
webpack,
runtimeTmpDir,
userConfigHash,
getExpandedEnvs,
runtimeDefineVars = {},
getRoutesFile,
isServer = false,
} = options;
const {
mode,
externals = {},
publicPath = '/',
outputDir,
loaders = [],
plugins = [],
alias = {},
sourceMap,
middlewares,
proxy,
configureWebpack,
experimental,
hash,
minify,
minimizerOptions = {},
enableCache = true,
cacheDir,
https,
analyzer,
tsCheckerOptions,
eslintOptions,
entry,
output = {},
splitChunks,
assetsManifest,
concatenateModules,
devServer,
fastRefresh,
logging,
swcOptions,
compileIncludes,
optimization = {},
performance,
enableCopyPlugin,
polyfill,
enableRpx2Vw = true,
enableEnv = true,
definitions = {},
webpackTarget,
externalsPresets,
} = config;
const absoluteOutputDir = path.isAbsolute(outputDir) ? outputDir : path.join(rootDir, outputDir);
const dev = mode !== 'production';
const hashKey = hash === true ? 'hash:8' : (hash || '');
const aliasWithRoot = getAliasWithRoot(rootDir, alias);
const defineVarsWithRuntime = getDefineVars(config.define, runtimeDefineVars, getExpandedEnvs, webpack);
const lazyCompilationConfig = dev && experimental?.lazyCompilation ? {
lazyCompilation: {
test: (module: NormalModule) => {
// do not lazy for framework bundles
const frameworkRegex = new RegExp(`[\\\\/]node_modules[\\\\/](${FRAMEWORK_BUNDLES.join('|')})[\\\\/]`);
return !frameworkRegex.test(module?.resourceResolveData?.path);
},
...(typeof experimental?.lazyCompilation === 'object' ? { ...experimental.lazyCompilation } : {}),
},
} : {};
// get compile plugins
const compilerWebpackPlugins = getCompilerPlugins(rootDir, config, 'webpack', { isServer });
const terserOptions: any = merge({
compress: {
ecma: 5,
unused: true,
// The following two options are known to break valid JavaScript code
// https://github.com/vercel/next.js/issues/7178#issuecomment-493048965
comparisons: false,
inline: 2,
passes: 4,
},
// If JSMinifier is swc remove mangle config because it will cause minification error.
mangle: minify === JSMinifier.swc ? {} : {
safari10: true,
},
format: {
safari10: true,
comments: false,
// Fixes usage of Emoji and certain Regex
ascii_only: true,
},
module: true,
}, minimizerOptions);
const supportBrowsers = getSupportedBrowsers(rootDir, dev);
const browsersMD5 = supportBrowsers ? crypto.createHash('md5').update(supportBrowsers.join('')).digest('hex') : '';
const compilation = compilationPlugin({
rootDir,
cacheDir,
sourceMap,
fastRefresh,
mode,
compileIncludes,
compileExcludes,
swcOptions,
polyfill,
enableEnv,
getRoutesFile,
});
const webpackConfig = {
mode,
target: webpackTarget,
externalsPresets,
experiments: {
layers: true,
cacheUnaffected: true,
topLevelAwait: true,
...lazyCompilationConfig,
...(experimental || {}),
},
entry: entry || (() => getEntry(rootDir, runtimeTmpDir)),
externals,
output: {
clean: true,
publicPath,
path: absoluteOutputDir,
filename: `js/${hashKey ? `[name]-[${hashKey}].js` : '[name].js'}`,
assetModuleFilename: 'assets/[name].[hash:8][ext]',
...output,
},
context: rootDir,
module: {
parser: {
javascript: {
importExportsPresence: 'warn',
exportsPresence: 'warn',
},
},
rules: [
// Use webpack loader instead of webpack plugin which is generated by unplugin.
// Reason: https://github.com/unjs/unplugin/issues/154
{
test: compilation.transformInclude,
use: {
loader: require.resolve('@ice/shared-config/compilation-loader'),
options: {
transform: compilation.transform,
},
},
},
...loaders,
],
},
resolve: {
alias: aliasWithRoot,
symlinks: true,
extensions: ['.ts', '.tsx', '.jsx', '...'],
fallback: {
// TODO: add more fallback module
events: require.resolve('events'),
stream: false,
fs: false,
path: false,
},
conditionNames: (config.target ? [config.target] : []).concat(['...']),
},
resolveLoader: {
modules: ['node_modules'],
},
watchOptions: {
// add a delay before rebuilding once routes changed
// webpack can not be found routes component after it has been deleted
aggregateTimeout: 200,
ignored: watchIgnoredRegexp,
},
optimization: {
splitChunks: typeof splitChunks == 'object'
? splitChunks
: getSplitChunksConfig(rootDir, splitChunks),
minimize: !!minify,
minimizer: [
new TerserPlugin({
// Minify of swc is still experimental, config `minify: 'swc'` faster minification.
minify: minify === JSMinifier.swc ? TerserPlugin.swcMinify : TerserPlugin.terserMinify,
extractComments: false,
terserOptions,
}),
new CssMinimizerPlugin({
parallel: false,
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true },
},
],
},
}),
],
...optimization,
} as Configuration['optimization'],
cache: enableCache ? {
type: 'filesystem',
version: `${process.env.__ICE_VERSION__}|${userConfigHash}|${browsersMD5}`,
buildDependencies: { config: [path.join(rootDir, 'package.json')] },
cacheDirectory: path.join(cacheDir, 'webpack'),
} : false,
// custom stat output by stats.toJson() calls in plugin-app
stats: 'none',
infrastructureLogging: {
level: 'warn',
},
performance: performance || false,
devtool: getDevtoolValue(sourceMap),
plugins: [
...plugins,
...compilerWebpackPlugins,
// @ts-ignore
!dev && new EnvReplacementPlugin(),
dev && fastRefresh && new ReactRefreshWebpackPlugin({
exclude: [/node_modules/, /bundles[\\\\/]compiled/],
// use webpack-dev-server overlay instead
overlay: false,
}),
new webpack.ProvidePlugin({
process: require.resolve('process/browser'),
...definitions,
}),
// server don't need runtimeDefine
new webpack.DefinePlugin(isServer ? config.define : defineVarsWithRuntime),
assetsManifest && new AssetsManifestPlugin({
fileName: 'assets-manifest.json',
outputDir: path.join(rootDir, runtimeTmpDir),
}),
analyzer && new BundleAnalyzerPlugin(),
tsCheckerOptions && new ForkTsCheckerPlugin(tsCheckerOptions),
eslintOptions && new ESlintPlugin(eslintOptions),
// copy plugin only active in production
// otherwise it will add assets to webpack compilation
(enableCopyPlugin || !dev) && new CopyPlugin({
patterns: [{
from: path.join(rootDir, 'public'),
to: absoluteOutputDir,
// ignore assets already in compilation.assets such as js and css files
force: false,
noErrorOnMissing: true,
// Skip minimization by default.
info: {
minimized: true,
},
globOptions: {
dot: true,
gitignore: true,
},
}],
}),
].filter(Boolean),
devServer: merge({
liveReload: false,
allowedHosts: 'all',
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
},
proxy,
hot: true,
compress: false,
webSocketServer: 'ws',
devMiddleware: {
publicPath,
},
static: {
watch: {
ignored: watchIgnoredRegexp,
},
},
client: {
overlay: {
errors: true,
warnings: false,
runtimeErrors: (error) => {
// Ignore hydration error in webpack-dev-server overlay.
// FIXME when hydration error is not occurred in HMR.
if (['Hydration failed', 'server-rendered HTML', 'Text content did not match', 'There was an error while hydrating'].some((text) => error.message.includes(text))) {
return false;
}
return true;
},
},
logging: 'info',
},
setupMiddlewares: middlewares,
https,
}, devServer || {}) as Config['devServer'],
} as Configuration;
// tnpm / cnpm 安装时webpack 5 的持久缓存无法生成,长时间将导致 OOM
// 原因:[managedPaths](https://webpack.js.org/configuration/other-options/#managedpaths) 在 tnpm / cnpm 安装的情况下失效,导致持久缓存在处理 node_modules
// 通过指定 [immutablePaths](https://webpack.js.org/configuration/other-options/#immutablepaths) 进行兼容
// 依赖路径中同时包含包名和版本号即可满足 immutablePaths 的使用
// 通过安装后的 package.json 中是否包含 __npminstall_done 字段来判断是否为 tnpm / cnpm 安装模式
if (require('../package.json').__npminstall_done) {
const nodeModulesPath = path.join(rootDir, 'node_modules');
webpackConfig.snapshot = {
immutablePaths: [nodeModulesPath],
};
}
if (dev && !concatenateModules) {
if (!webpackConfig.optimization) {
webpackConfig.optimization = {};
}
// do not figure out file exports when dev
webpackConfig.optimization.providedExports = false;
webpackConfig.optimization.usedExports = false;
}
if (logging) {
const infra = logging.includes('infrastructure');
const profile = logging.includes('profile');
const summary = logging.includes('summary');
const assets = logging.includes('assets');
if (infra) {
webpackConfig.infrastructureLogging = {
level: 'verbose',
debug: /FileSystemInfo/,
};
webpackConfig.stats = 'verbose';
}
if (profile || summary) {
webpackConfig.plugins!.push((compiler: Compiler) => {
compiler.hooks.done.tap('webpack-logging', (stats) => {
console.log(
stats.toString(profile ? {
colors: true,
logging: 'verbose',
} : {
preset: 'summary',
assets,
colors: true,
timings: true,
}),
);
});
});
}
if (profile) {
const ProgressPlugin = webpack.ProgressPlugin as typeof webpack.ProgressPlugin;
webpackConfig.plugins!.push(
new ProgressPlugin({
profile: true,
}),
);
webpackConfig.profile = true;
}
}
// pipe webpack by built-in functions and custom functions
const ctx = {
...config,
rootDir,
hashKey,
webpack,
bundler: webpack,
enableRpx2Vw,
};
return [configCss, configAssets, ...(configureWebpack || [])]
.reduce((result, next: ModifyWebpackConfig<Configuration, typeof webpack>) => next(result, ctx), webpackConfig);
}