mirror of https://github.com/alibaba/ice.git
441 lines
14 KiB
TypeScript
441 lines
14 KiB
TypeScript
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);
|
||
}
|
||
|