-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: split server chunk #7040
base: release/next
Are you sure you want to change the base?
feat: split server chunk #7040
Changes from all commits
264f512
0f7ee36
b7111f9
60bdcbb
252eb4b
fe58611
490e1da
9d585da
3f0d671
a6fa7e1
8b2efbe
40876d5
eb2e5db
4ef7f8d
6a5b317
a56748b
4edab02
cc6617e
8ca5444
36314c5
c5df87b
9f32548
8eb8167
f49a02d
f8609f4
8a677be
22fafbe
1965e16
73e506c
2ac13ec
fbef850
f93a678
bd0faf8
7800077
ce34573
579b338
4258ec2
e367461
9567a92
f690570
2a20982
b14428a
a0cd991
6ffb452
af6d933
50c3234
121348a
5760e3f
d7827e0
ed0c302
2ac6d99
cb689c0
51182cb
7e62ad3
7c6b8a7
02cd737
bb08f88
dc44e7a
cb55a64
bc0266a
d297a07
e50b1e5
eb8ee79
68b1b82
f999e74
f373eeb
b2a6aa2
a994968
8d85cc3
6354d1d
917268e
4b5429e
8e6ceee
0017d9c
8f4b5c0
9b0576a
4208450
b8b4c92
e33705b
39eb3f5
bc0c3a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'@ice/bundles': patch | ||
'@ice/app': patch | ||
--- | ||
|
||
@ice/app: align the output result with the former esbuild | ||
@ice/bundles: export more webpack internal modules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'@ice/bundles': patch | ||
'@ice/app': patch | ||
--- | ||
|
||
@ice/app: remove unused deps and import them from @ice/bundles | ||
@ice/bundles: compile tsconfig-paths-webpack-plugin |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@ice/app': patch | ||
--- | ||
|
||
rebase releast/next |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'@ice/app': patch | ||
--- | ||
|
||
feat: minify css file; | ||
feat: change minifier from terser to esbuildMinifier. | ||
feat: support config minify option |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
'@ice/app': patch | ||
--- | ||
|
||
- feat: change transformInclude to array | ||
- fix: only treat .js as jsx | ||
- feat: support customize webpack.module.rule | ||
- feat: support handle assets |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@ice/app': minor | ||
--- | ||
|
||
support split server bundle |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./bundle').JavascriptModulesPlugin; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./bundle').StartupChunkDependenciesPlugin; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./bundle').StartupHelpers; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./bundle').compileBooleanMatcher; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('./bundle').identifier; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -108,4 +108,4 @@ | |
"publishConfig": { | ||
"access": "public" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,8 @@ import getCSSModuleIdent from '../utils/getCSSModuleIdent.js'; | |
import { scanImports } from './analyze.js'; | ||
import type { PreBundleDepsMetaData } from './preBundleDeps.js'; | ||
import preBundleDeps from './preBundleDeps.js'; | ||
import { WebpackServerCompiler } from './webpackServerCompiler/compiler.js'; | ||
import VirualAssetPlugin from './webpackServerCompiler/virtualAssetPlugin.js'; | ||
|
||
const logger = createLogger('server-compiler'); | ||
|
||
|
@@ -100,7 +102,6 @@ export function createServerCompiler(options: Options) { | |
const externals = task.config?.externals || {}; | ||
const sourceMap = task.config?.sourceMap; | ||
const dev = command === 'start'; | ||
|
||
// Filter empty alias. | ||
const { ignores, alias } = filterAlias(task.config?.alias || {}); | ||
|
||
|
@@ -144,6 +145,20 @@ export function createServerCompiler(options: Options) { | |
redirectImports, | ||
getRoutesFile, | ||
}, 'esbuild', { isServer }); | ||
const transformWebpackPlugins = getCompilerPlugins( | ||
rootDir, | ||
{ | ||
...task.config, | ||
fastRefresh: false, | ||
enableEnv, | ||
polyfill: false, | ||
swcOptions, | ||
redirectImports, | ||
getRoutesFile, | ||
}, | ||
'webpack', | ||
{ isServer }, | ||
); | ||
const define = getRuntimeDefination(task.config?.define || {}, runtimeDefineVars, transformEnv); | ||
if (preBundle) { | ||
const plugins = [ | ||
|
@@ -171,7 +186,6 @@ export function createServerCompiler(options: Options) { | |
} | ||
|
||
const format = customBuildOptions?.format || 'esm'; | ||
|
||
let buildOptions: esbuild.BuildOptions = { | ||
bundle: true, | ||
format, | ||
|
@@ -245,7 +259,30 @@ export function createServerCompiler(options: Options) { | |
context = await esbuild.context(buildOptions); | ||
esbuildResult = await context.rebuild(); | ||
} else { | ||
esbuildResult = await esbuild.build(buildOptions); | ||
if (Array.isArray(buildOptions.entryPoints)) { | ||
// this build phase is aimed to generate .ice/ | ||
esbuildResult = await esbuild.build(buildOptions); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 为啥多 entry 必定是 esbuild |
||
} else { | ||
switch (server.bundler) { | ||
case 'webpack': | ||
const webpackServerCompiler = new WebpackServerCompiler({ | ||
...buildOptions, | ||
externals, | ||
plugins: [ | ||
compilationInfo && new VirualAssetPlugin({ compilationInfo, rootDir }), | ||
...transformWebpackPlugins, | ||
].filter(Boolean), | ||
rootDir, | ||
userServerConfig: server, | ||
}); | ||
esbuildResult = (await webpackServerCompiler.build())?.compilation; | ||
break; | ||
case 'esbuild': | ||
default: | ||
esbuildResult = await esbuild.build(buildOptions); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
logger.debug('[esbuild]', `time cost: ${new Date().getTime() - startTime}ms`); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,314 @@ | ||
import { createRequire } from 'module'; | ||
import path from 'path'; | ||
import { fileURLToPath } from 'url'; | ||
import { esbuild, less, sass } from '@ice/bundles'; | ||
import MiniCssExtractPlugin from '@ice/bundles/compiled/mini-css-extract-plugin/dist/index.js'; | ||
import TerserPlugin from '@ice/bundles/compiled/terser-webpack-plugin/index.js'; | ||
import { getCSSModuleLocalIdent, getPostcssOpts } from '@ice/shared-config'; | ||
import type { Config } from '@ice/shared-config/types'; | ||
import TsconfigPathsPlugin from '@ice/bundles/compiled/tsconfig-paths-webpack-plugin/index.js'; | ||
import CssMinimizerPlugin from '@ice/bundles/compiled/css-minimizer-webpack-plugin/index.js'; | ||
import webpack, { type LoaderContext } from 'webpack'; | ||
import type { UserConfig } from '../../types/userConfig.js'; | ||
import { logger } from '../../utils/logger.js'; | ||
|
||
const require = createRequire(import.meta.url); | ||
const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); | ||
interface Options { | ||
publicPath: string; | ||
postcssOptions: Config['postcss']; | ||
rootDir: string; | ||
enableRpx2Vw: boolean; | ||
cssModules: Config['cssModules']; | ||
} | ||
|
||
type CSSRuleConfig = [string, string?, Record<string, any>?]; | ||
|
||
export const ASSET_TYPES = [ | ||
// images | ||
'png', | ||
'jpe?g', | ||
'gif', | ||
'svg', | ||
'ico', | ||
'webp', | ||
'avif', | ||
|
||
// media | ||
'mp4', | ||
'webm', | ||
'ogg', | ||
'mp3', | ||
'wav', | ||
'flac', | ||
'aac', | ||
|
||
// fonts | ||
'woff2?', | ||
'eot', | ||
'ttf', | ||
'otf', | ||
|
||
// other | ||
'wasm', | ||
'webmanifest', | ||
'pdf', | ||
'txt', | ||
]; | ||
|
||
const ASSETS_RE = new RegExp(`\\.(${ASSET_TYPES.join('|')})(\\?.*)?$`); | ||
|
||
export class WebpackServerCompiler { | ||
private config: webpack.Configuration; | ||
private options: { userServerConfig: UserConfig['server']; [key: string]: any }; | ||
|
||
constructor(options: any) { | ||
this.options = options; | ||
this.config = this.createWebpackConfig(options); | ||
} | ||
private configCSSRule(config: CSSRuleConfig, options: Options) { | ||
const { publicPath, rootDir, enableRpx2Vw, postcssOptions: userPostcssOptions, cssModules } = options; | ||
const [style, loader, loaderOptions] = config; | ||
const cssLoaderOpts = { | ||
sourceMap: false, | ||
}; | ||
const cssModuleLoaderOpts = { | ||
...cssLoaderOpts, | ||
modules: { | ||
auto: (resourcePath: string) => resourcePath.endsWith(`.module.${style}`), | ||
getLocalIdent: (context: LoaderContext<any>, localIdentName: string, localName: string) => { | ||
return getCSSModuleLocalIdent(context.resourcePath, localName, cssModules?.localIdentName); | ||
}, | ||
}, | ||
}; | ||
const postcssOpts = getPostcssOpts({ rootDir, userPostcssOptions, enableRpx2Vw }); | ||
return { | ||
test: new RegExp(`\\.${style}$`), | ||
use: [ | ||
{ | ||
loader: MiniCssExtractPlugin.loader, | ||
// compatible with commonjs syntax: const styles = require('./index.module.less') | ||
options: { | ||
esModule: false, | ||
publicPath, | ||
}, | ||
}, | ||
{ | ||
loader: require.resolve('@ice/bundles/compiled/css-loader'), | ||
options: cssModuleLoaderOpts, | ||
}, | ||
{ | ||
loader: require.resolve('@ice/bundles/compiled/postcss-loader'), | ||
options: { | ||
...cssLoaderOpts, | ||
...postcssOpts, | ||
}, | ||
}, | ||
loader && { | ||
loader, | ||
options: { ...cssLoaderOpts, ...loaderOptions }, | ||
}, | ||
].filter(Boolean), | ||
}; | ||
} | ||
|
||
private createWebpackConfig(options: { | ||
userServerConfig: UserConfig['server']; | ||
[key: string]: any; | ||
}): webpack.Configuration { | ||
const { userServerConfig } = options; | ||
const { webpackConfig = {} } = userServerConfig; | ||
const cssRules = [ | ||
['css'], | ||
[ | ||
'less', | ||
require.resolve('@ice/bundles/compiled/less-loader'), | ||
{ | ||
lessOptions: { javascriptEnabled: true }, | ||
implementation: less, | ||
}, | ||
], | ||
[ | ||
'scss', | ||
require.resolve('@ice/bundles/compiled/sass-loader'), | ||
{ | ||
implementation: sass, | ||
}, | ||
], | ||
].map((config: any) => | ||
this.configCSSRule(config, { | ||
publicPath: '/', | ||
postcssOptions: {}, | ||
rootDir: process.cwd(), | ||
enableRpx2Vw: true, | ||
cssModules: {}, | ||
}), | ||
); | ||
const cssOutputFolder = 'css'; | ||
const hashKey = ''; | ||
const cssFilename = undefined; | ||
const cssChunkFilename = undefined; | ||
|
||
for (const key of Object.keys(options.alias)) { | ||
if (options.alias[key].startsWith('./')) { | ||
options.alias[key] = path.resolve(options.rootDir, options.alias[key]); | ||
} | ||
} | ||
return { | ||
mode: 'production', | ||
entry: options.entryPoints as string[], | ||
target: 'node12.20', | ||
externalsPresets: { | ||
node: false, | ||
}, | ||
output: { | ||
filename: `[name].${options.format === 'esm' ? 'mjs' : 'cjs'}`, | ||
path: options.outdir, | ||
// align the output with former esbuild | ||
chunkFormat: false, | ||
clean: true, | ||
library: { | ||
type: 'commonjs2', | ||
}, | ||
...(webpackConfig.output as any), | ||
}, | ||
devtool: 'source-map', | ||
externals: options.externals, | ||
optimization: { | ||
minimize: !!options.minify, | ||
minimizer: [ | ||
new TerserPlugin({ | ||
extractComments: false, | ||
terserOptions: typeof options.minify === 'object' ? options.minify : undefined, | ||
minify: TerserPlugin.esbuildMinify, | ||
}), | ||
new CssMinimizerPlugin({ | ||
parallel: false, | ||
minimizerOptions: { | ||
preset: [ | ||
'default', | ||
{ | ||
discardComments: { removeAll: true }, | ||
}, | ||
], | ||
}, | ||
}), | ||
], | ||
...(webpackConfig.optimization as any), | ||
}, | ||
resolve: { | ||
alias: options.alias, | ||
fallback: { crypto: false }, | ||
extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '...'], | ||
plugins: [ | ||
new TsconfigPathsPlugin({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个是必要插件吗,理论上编译不受 tsconfig 影响 |
||
/* options: see below */ | ||
}), | ||
], | ||
}, | ||
module: { | ||
rules: [ | ||
// Use esbuild to compile JavaScript & TypeScript | ||
{ | ||
// // Match `.js`, `.jsx`, `.ts` or `.tsx` files | ||
test: /\.m?[jt]sx?$/, | ||
exclude(path) { | ||
if (path.includes('node_modules')) { | ||
if (webpackConfig.transformInclude) { | ||
return !webpackConfig.transformInclude.some((i) => { | ||
if (i instanceof RegExp) { | ||
return i.test(path); | ||
} else if (typeof i === 'string') { | ||
return path.includes(i); | ||
} else { | ||
return false; | ||
} | ||
}); | ||
} else { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}, | ||
use: [ | ||
(info: any) => { | ||
const ext = path.extname(info.resource); | ||
return { | ||
loader: '@ice/bundles/compiled/esbuild-loader', | ||
// available options: https://github.com/evanw/esbuild/blob/88821b7e7d46737f633120f91c65f662eace0bcf/lib/shared/types.ts#L158-L172 | ||
options: { | ||
target: options.target, | ||
// make sure tree shaking is worked | ||
format: 'esm', | ||
loader: ext === '.js' ? 'jsx' : 'default', | ||
jsx: options.jsx, | ||
jsxImportSource: '@ice/runtime/react', | ||
sourcemap: options.sourcemap, | ||
define: options.define, | ||
// banner can only be string in transform mode | ||
banner: options.banner?.js, | ||
implementation: esbuild, | ||
}, | ||
}; | ||
}, | ||
path.resolve(_dirname, 'removeMagicString.js'), | ||
], | ||
}, | ||
{ | ||
test: ASSETS_RE, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. webpack 大部分规则是不是跟 csr 对齐就好了,没必要单独这里维护,对于 node 端而言主要是产物 target 和样式处理规则的差异 |
||
type: 'asset/inline', | ||
}, | ||
...cssRules, | ||
...(webpackConfig.module?.rules || []), | ||
], | ||
}, | ||
plugins: [ | ||
...options.plugins, | ||
new webpack.DefinePlugin(options.define), | ||
new webpack.SourceMapDevToolPlugin({ | ||
// remove append sourcemap comment | ||
append: false, | ||
filename: '[file].map', | ||
moduleFilenameTemplate: '[absolute-resource-path]', | ||
}), | ||
new MiniCssExtractPlugin({ | ||
filename: cssFilename || `${cssOutputFolder}/${hashKey ? `[name]-[${hashKey}].css` : '[name].css'}`, | ||
chunkFilename: cssChunkFilename || `${cssOutputFolder}/${hashKey ? `[name]-[${hashKey}].css` : '[name].css'}`, | ||
// If the warning is triggered, it seen to be unactionable for the user, | ||
ignoreOrder: true, | ||
}), | ||
...(webpackConfig.plugins || []), | ||
], | ||
stats: { | ||
errorDetails: true, | ||
}, | ||
}; | ||
} | ||
|
||
private async handleEsbuildInject() { | ||
const provideRecord = {}; | ||
const allInjects = await Promise.all(this.options.inject.map((inj) => import(inj))); | ||
allInjects.forEach((injs, index) => { | ||
Object.keys(injs).forEach((key) => { | ||
provideRecord[key] = [this.options.inject[index], key]; | ||
}); | ||
}); | ||
return new webpack.ProvidePlugin(provideRecord); | ||
} | ||
|
||
async build(): Promise<any> { | ||
const providePlugin = await this.handleEsbuildInject(); | ||
this.config.plugins.push(providePlugin); | ||
return new Promise((resolve, reject) => { | ||
webpack(this.config, (err, stats) => { | ||
if (err || stats?.hasErrors?.()) { | ||
logger.error(err || stats.toString()); | ||
reject(err || stats.toString()); | ||
process.exit(1); | ||
} else { | ||
resolve(stats); | ||
} | ||
}); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export default function (source: string) { | ||
const result = source.replace( | ||
/webpackChunkName:\s*["'][^"']+["']/g, | ||
'webpackMode: "eager"', | ||
); | ||
|
||
// Return the modified source | ||
return result; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { type AssetsManifest } from '@ice/runtime/types'; | ||
import NormalModule from 'webpack/lib/NormalModule.js'; | ||
import { type Compiler } from '@ice/bundles/compiled/webpack'; | ||
|
||
interface CompilationInfo { | ||
assetsManifest?: AssetsManifest; | ||
} | ||
|
||
const PLUGIN_NAME = 'VirtualManifestPlugin'; | ||
class VirtualManifestPlugin { | ||
private rootDir: string; | ||
private compilationInfo: CompilationInfo | (() => CompilationInfo); | ||
|
||
constructor(options) { | ||
this.rootDir = options.rootDir; | ||
this.compilationInfo = options.compilationInfo; | ||
} | ||
|
||
apply(compiler: Compiler) { | ||
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation, { normalModuleFactory }) => { | ||
NormalModule.getCompilationHooks(compilation) | ||
.readResource.for('virtual') | ||
.tap(PLUGIN_NAME, () => { | ||
const manifest = this.generateManifestContent(); | ||
return JSON.stringify(manifest?.assetsManifest || ''); | ||
}); | ||
normalModuleFactory.hooks.beforeResolve.tap(PLUGIN_NAME, (resolveData) => { | ||
if (resolveData.request === 'virtual:assets-manifest.json') { | ||
resolveData.assertions = { | ||
type: 'json', | ||
}; | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
generateManifestContent() { | ||
return typeof this.compilationInfo === 'function' ? this.compilationInfo() : this.compilationInfo; | ||
} | ||
} | ||
|
||
export default VirtualManifestPlugin; |
Large diffs are not rendered by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
没必要同时获取 transformWebpackPlugins 和 esbuildPlugins