diff --git a/.gitignore b/.gitignore index 0006fe3..eafc18f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,23 +6,4 @@ package-lock.json index.d.ts .DS_Store dist -docs/.vitepress/cache/deps/_metadata.json -docs/.vitepress/cache/deps/@theme_index.js -docs/.vitepress/cache/deps/@theme_index.js.map -docs/.vitepress/cache/deps/chunk-HWKKPLDN.js -docs/.vitepress/cache/deps/chunk-HWKKPLDN.js.map -docs/.vitepress/cache/deps/chunk-VDV77W7A.js -docs/.vitepress/cache/deps/chunk-VDV77W7A.js.map -docs/.vitepress/cache/deps/package.json -docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js -docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map -docs/.vitepress/cache/deps/vitepress___@vueuse_core.js -docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map -docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js -docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map -docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js -docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map -docs/.vitepress/cache/deps/vitepress___minisearch.js -docs/.vitepress/cache/deps/vitepress___minisearch.js.map -docs/.vitepress/cache/deps/vue.js -docs/.vitepress/cache/deps/vue.js.map +docs/.vitepress/cache diff --git a/README.md b/README.md index 5d5a418..5575f09 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ # rzpack ### 介绍 - `Rzpack`是一款基于`Webpack5`开发的React打包工具,通过`Webpack5`的`cache`、`lazyCompilation`特性及`esbuild`、`swc`等工具的配合,大大提高开发环境的启动速度,热更速度及打包速度,内置了许多功能,无需复杂的配置即可快速开发。同时配套的`create-rzpack`可以快速创建项目模板,省去项目框架搭建的时间。 + `Rzpack`是一款基于`Webpack5`/`Rspack`开发的React打包工具,通过`Webpack5`的`cache`、`lazyCompilation`特性及`esbuild`、`swc`等工具的配合,或者使用由`Rust`开发的`Rspack`,大大提高开发环境的启动速度,热更速度及打包速度,内置了许多功能,无需复杂的配置即可快速开发。同时配套的`create-rzpack`可以快速创建项目模板,省去项目框架搭建的时间。 ### 环境配置 @@ -141,27 +141,34 @@ pnpm create rzpack rzpack-app --template react-ts ### 配置 -| 属性 | 说明 | 类型 | 默认 | 是否必填 | -| ---------------- | -------------------------------------------------- | ------------------------------------------ | ---------------- | -------- | -| antdTheme | antd主题变量设置 | `LessVars` | - | 非必填 | -| lessVars | less全局变量设置 | `LessVars` | - | 非必填 | -| assets | 资源文件处理 | `RzpackAssets` | - | 非必填 | -| buildInfo | 是否在控制台打印编译信息 | `boolean\|BuildInfoWebpackPluginOptions` | - | 非必填 | -| cache | 是否使用webpack5缓存 | boolean | true | 非必填 | -| entry | 打包入口 | `string\|string[]\|Record` | `./src/main.tsx` | 非必填 | -| gzip | 是否启用gzip | boolean | - | 非必填 | -| html | htmlPlugin插件设置(配置参考htmlWebpackPlugin插件) | `HtmlWebpackPlugin.Options` | - | 非必填 | -| output | 输出目录 | `Output` | `dist` | 非必填 | -| publicPath | 静态资源目录 | string | `public` | 非必填 | -| server | 代理配置,当开启可视化配置时此处配置的接口代理无效 | `WebpackDevServerConfiguration` | - | 非必填 | -| lazyCompilation | 实验性功能 | `LazyCompilationOptions` | - | 非必填 | -| moduleFederation | 模块联邦 | `ModuleFederationPluginOptions` | - | 非必填 | -| webpackChain | 使用webpackChain重写webpack配置 | `RzpackWebpackChain` | - | 非必填 | -| proxyFile | 可视化配置的代理,仅在开启可视化配置时才生效 | string | - | 非必填 | -| reactRefresh | 是否开启React代码热更新 | boolean | - | 非必填 | -| million | 是否使用Million.js | `boolean\|MillionOptions` | - | 非必填 | +| 属性 | 说明 | 类型 | 默认 | 是否必填 | +| ---------------- | ----------------------------------------------------------------------------------- | ------------------------------------------ | ----------------- | -------- | +| builder | 打包器 | `BUILDER` | `BUILDER.WEBPACK` | 非必填 | +| antdTheme | antd主题变量设置 | `LessVars` | - | 非必填 | +| lessVars | less全局变量设置 | `LessVars` | - | 非必填 | +| assets | 资源文件处理 | `RzpackAssets` | - | 非必填 | +| buildInfo | 是否在控制台打印编译信息 | `boolean\|BuildInfoWebpackPluginOptions` | - | 非必填 | +| cache | 是否使用持久化缓存(目前Webpack仅支持) | boolean | true | 非必填 | +| entry | 打包入口 | `string\|string[]\|Record` | `./src/main.tsx` | 非必填 | +| gzip | 是否启用gzip | boolean | - | 非必填 | +| html | htmlPlugin/HtmlRspackPlugin插件设置(配置参考htmlWebpackPlugin/HtmlRspackPlugin插件) | `HtmlWebpackPlugin.Options` | - | 非必填 | +| output | 输出目录 | `Output` | `dist` | 非必填 | +| publicPath | 静态资源目录 | string | `public` | 非必填 | +| server | 代理配置,当开启可视化配置时此处配置的接口代理无效 | `WebpackDevServerConfiguration` | - | 非必填 | +| lazyCompilation | 实验性功能 | `LazyCompilationOptions` | - | 非必填 | +| moduleFederation | 模块联邦 | `ModuleFederationPluginOptions` | - | 非必填 | +| webpackChain | 使用webpackChain重写webpack配置(0.2.x以下支持) | `RzpackWebpackChain` | - | 非必填 | +| rzpackChain | 使用webpackChain重写webpack/rspack配置(0.3.x支持) | `RzpackWebpackChain` | - | 非必填 | +| proxyFile | 可视化配置的代理,仅在开启可视化配置时才生效 | string | - | 非必填 | +| reactRefresh | 是否开启React代码热更新 | boolean | - | 非必填 | +| million | 是否使用Million.js | `boolean\|MillionOptions` | - | 非必填 | ```ts +export enum BUILDER { + WEBPACK = 'webpack', + RSPACK = 'rspack', +} + export interface LessVars { // 全局变量(直接定义的变量优先级高于变量文件) vars?: Record diff --git a/biome.json b/biome.json index 17bcc40..a358eec 100644 --- a/biome.json +++ b/biome.json @@ -1,18 +1,21 @@ { - "$schema": "https://biomejs.dev/schemas/1.1.2/schema.json", - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, + "files": { "ignore": [ "index.d.ts", "bin", "dist", "packages/create-rzpack/template-*/**", "packages/ui/client", - "scripts" - ], + "scripts", + "playground" + ] + }, + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, "rules": { "recommended": true, "correctness": { @@ -46,11 +49,6 @@ "formatWithErrors": false, "indentStyle": "space", "indentWidth": 2, - "lineWidth": 80, - "ignore": [ - "packages/create-rzpack/template-*/**", - "packages/ui/client", - "dist" - ] + "lineWidth": 80 } } diff --git a/docs/pages/configs.md b/docs/pages/configs.md index 9d08ece..74f7883 100644 --- a/docs/pages/configs.md +++ b/docs/pages/configs.md @@ -1,5 +1,21 @@ # 配置 +## builder 打包器 + +- 类型:`BUILDER` +- 默认:`BUILDER.WEBPACK` + +> 目前`Rspack`尚未达到生产可用状态,可以尝鲜,生产环境慎用。 + +```ts +export enum BUILDER { + WEBPACK = 'webpack', + RSPACK = 'rspack', +} +``` + +设置打包器。 + ## antdTheme 主题配置 - 类型:`LessVars` diff --git a/docs/pages/guide/cli.md b/docs/pages/guide/cli.md index 0a7afd1..d9a0df7 100644 --- a/docs/pages/guide/cli.md +++ b/docs/pages/guide/cli.md @@ -40,15 +40,16 @@ rzpack build [root] #### 选项 -| 属性 | 说明 | 类型 | 默认 | -| ----------------------- | ------------------- | --------- | -------------------- | -| --c | 指定配置文件 | `string` | `./vigour.config.ts` | -| --config | 指定配置文件 | `string` | `./vigour.config.ts` | -| --m | 指定webpack启动模式 | `string` | `development` | -| --mode | 指定webpack启动模式 | `string` | `development` | -| --outDir [dir] | 输出目录 | `string` | `dist` | -| --bundle-size [boolean] | 分析打包资源大小 | `boolean` | - | -| --bundle-time [boolean] | 分析打包时长 | `boolean` | - | +| 属性 | 说明 | 类型 | 默认 | 版本 | +| ----------------------- | -------------------------- | --------- | -------------------- | ----- | +| --c | 指定配置文件 | `string` | `./vigour.config.ts` | 0.2.x | +| --config | 指定配置文件 | `string` | `./vigour.config.ts` | 0.2.x | +| --m | 指定webpack启动模式 | `string` | `development` | 0.2.x | +| --mode | 指定webpack启动模式 | `string` | `development` | 0.2.x | +| --outDir [dir] | 输出目录 | `string` | `dist` | 0.2.x | +| --bundle-size [boolean] | 分析打包资源大小 | `boolean` | - | 0.2.x | +| --bundle-time [boolean] | 分析打包时长(rspack不支持) | `boolean` | - | 0.2.x | +| --doctor [boolean] | 可视化构建分析工具 | `boolean` | - | 0.3 | ## 预览 diff --git a/docs/pages/guide/what-is.md b/docs/pages/guide/what-is.md index e7dd6e5..917ab9f 100644 --- a/docs/pages/guide/what-is.md +++ b/docs/pages/guide/what-is.md @@ -1,3 +1,3 @@ # 什么是rzpack? -`rzpack`是一款基于`Webpack5`开发的React打包工具,通过`Webpack5`的`cache`、`lazyCompilation`特性及`esbuild`、`swc`等工具的配合,大大提高开发环境的启动速度,热更速度及打包速度,内置了许多功能,无需复杂的配置即可快速开发。同时配套`create-rzpack`可以快速创建项目模板,省去项目框架搭建的时间。 +`Rzpack`是一款基于`Webpack5`/`Rspack`开发的React打包工具,通过`Webpack5`的`cache`、`lazyCompilation`特性及`esbuild`、`swc`等工具的配合,或者使用由`Rust`开发的`Rspack`,大大提高开发环境的启动速度,热更速度及打包速度,内置了许多功能,无需复杂的配置即可快速开发。同时配套的`create-rzpack`可以快速创建项目模板,省去项目框架搭建的时间。 \ No newline at end of file diff --git a/package.json b/package.json index 00e25c4..1d99246 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "build": "pnpm prebuild && pnpm --parallel --filter=!./packages/rzpack-utils --filter=!./playground build", "playground:dev": "pnpm --filter=./playground dev", "playground:build": "pnpm --filter=./playground build", + "playground:build:time": "pnpm --filter=./playground build:time", + "playground:build:size": "pnpm --filter=./playground build:size", + "playground:build:doctor": "pnpm --filter=./playground build:doctor", "playground:preview": "pnpm --filter=./playground preview", "prepare": "npx simple-git-hooks", "pub": "zx ./scripts/publish.mjs", @@ -37,9 +40,6 @@ "pre-commit": "npx lint-staged" }, "lint-staged": { - "packages/*/src/**/*.{js,ts}": [ - "biome check", - "biome format --write" - ] + "packages/*/src/**/*.{js,ts}": ["biome check", "biome format --write"] } } diff --git a/packages/rzpack/package.json b/packages/rzpack/package.json index 108d178..a70d93f 100644 --- a/packages/rzpack/package.json +++ b/packages/rzpack/package.json @@ -1,6 +1,6 @@ { "name": "rzpack", - "version": "0.2.7", + "version": "0.3.0", "description": "基于Webpack5封装的前端打包器", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -43,6 +43,10 @@ "@renzp/css-scoped-loader": "^0.0.16", "@renzp/jsx-scoped-loader": "^0.0.24", "@renzp/unplugin-build-info": "^1.0.0", + "@rsdoctor/rspack-plugin": "^0.2.4", + "@rsdoctor/webpack-plugin": "^0.2.4", + "@rspack/core": "^0.7.0", + "@rspack/dev-server": "^0.7.0", "@soda/friendly-errors-webpack-plugin": "^1.8.1", "@svgr/webpack": "^6.5.1", "@swc/core": "1.3.32", diff --git a/packages/rzpack/src/cli.ts b/packages/rzpack/src/cli.ts index d00f57d..81ad9b8 100644 --- a/packages/rzpack/src/cli.ts +++ b/packages/rzpack/src/cli.ts @@ -1,9 +1,15 @@ #!/usr/bin/env node import { cac } from 'cac' import { fileExists, logError, pathResolve } from 'rzpack-utils' -import type { BuildOptions, RzpackConfigs, ServerOptions } from '.' -import { NAME, VERSION } from './constant' +import { + BUILDER, + type BuildOptions, + type RzpackConfigs, + type ServerOptions, +} from '.' +import { DEFAULT_CONFIG, NAME, VERSION } from './constant' import { RzpackContext } from './ctx' +import { runRspackBuild, runRspackPreview, runRspackServer } from './rspack' import { runWebpackBuild, runWebpackPreview, runWebpackServer } from './webpack' export const rzpack = new RzpackContext() @@ -27,11 +33,16 @@ cli .action(async (_: string, options: ServerOptions) => { const { c, m, mode, ui = true, config, host, port, open } = options ?? {} process.env.NODE_ENV = m ?? mode ?? 'development' - rzpack.webpackChain.devServer.host(host).port(port).open(open) + rzpack.chain.devServer.host(host).port(port).open(open) try { const configs: RzpackConfigs = rzpack.loadConfigFile(c ?? config) await rzpack.configs(configs) - runWebpackServer(ui, configs?.proxyFile) + const run = + configs?.builder === BUILDER.WEBPACK + ? runWebpackServer + : runRspackServer + + run(ui, configs?.proxyFile) } catch (error) { logError(error) } @@ -43,26 +54,33 @@ cli .option('--outDir ', '[string] output directory (default: dist)') .option('--bundle-size', '[boolean] analysis package size') .option('--bundle-time', '[boolean] analyze packaging time') + .option('--doctor', '[boolean] start Rsdoctor') .action(async (options: BuildOptions) => { const { c, m, mode, config, - outDir = 'dist', + outDir = DEFAULT_CONFIG.OUTPUT, bundleSize, bundleTime, + doctor, } = options ?? {} process.env.NODE_ENV = m ?? mode ?? 'production' rzpack.bundleSize = bundleSize ?? false rzpack.bundleTime = bundleTime ?? false + rzpack.doctor = doctor ?? false + try { const configs = rzpack.loadConfigFile(c ?? config) if (!configs?.output) { configs.output = outDir } await rzpack.configs(configs) - runWebpackBuild(!rzpack.bundleTime) + const run = + configs?.builder === BUILDER.WEBPACK ? runWebpackBuild : runRspackBuild + + run(!rzpack.bundleTime) } catch (error) { logError(error) } @@ -73,7 +91,7 @@ cli .command('preview', 'preview for outDir') .option('--outDir ', '[string] output directory (default: dist)') .action(async (options: BuildOptions) => { - const { c, m, mode, config, outDir = 'dist' } = options ?? {} + const { c, m, mode, config, outDir = DEFAULT_CONFIG.OUTPUT } = options ?? {} process.env.NODE_ENV = m ?? mode ?? 'production' const configs = rzpack.loadConfigFile(c ?? config) @@ -88,10 +106,21 @@ cli let isPreview: boolean = fileExists(fullPath) if (!isPreview) { await rzpack.configs(configs) - isPreview = await runWebpackBuild(false) + const runBuild = + configs?.builder === BUILDER.WEBPACK ? runWebpackBuild : runRspackBuild + + isPreview = await runBuild(false) + } + + if (configs?.builder === BUILDER.WEBPACK) { + runWebpackPreview(dir) } + const run = + configs?.builder === BUILDER.WEBPACK + ? runWebpackPreview + : runRspackPreview - runWebpackPreview(dir) + run(dir) }) cli.help() diff --git a/packages/rzpack/src/webpack/configs/alias.ts b/packages/rzpack/src/common/configs/alias.ts similarity index 100% rename from packages/rzpack/src/webpack/configs/alias.ts rename to packages/rzpack/src/common/configs/alias.ts diff --git a/packages/rzpack/src/webpack/configs/entry.ts b/packages/rzpack/src/common/configs/entry.ts similarity index 80% rename from packages/rzpack/src/webpack/configs/entry.ts rename to packages/rzpack/src/common/configs/entry.ts index 4a26166..0229ed2 100644 --- a/packages/rzpack/src/webpack/configs/entry.ts +++ b/packages/rzpack/src/common/configs/entry.ts @@ -6,10 +6,12 @@ const getEntryKey = (entry: string) => entry .split('/') .pop() - .replace('.tsx', '') - .replace('.jsx', '') - .replace('.ts', '') - .replace('.js', '') + ?.replace('.tsx', '') + ?.replace('.jsx', '') + ?.replace('.ts', '') + ?.replace('.js', '') + +const EntryDefaultKey = 'main' export default ( webpackChain: WebpackChain, @@ -18,8 +20,7 @@ export default ( if (entry) { switch (true) { case typeof entry === 'string': { - // eslint-disable-next-line no-case-declarations - const key = getEntryKey(entry as string) + const key = getEntryKey(entry as string) ?? EntryDefaultKey webpackChain.entry(key).add(entry as string) break } @@ -34,7 +35,7 @@ export default ( ) process.exit(-1) } - const key = getEntryKey(item) + const key = getEntryKey(item) ?? EntryDefaultKey return { [key]: item, } @@ -53,6 +54,6 @@ export default ( } } } else { - webpackChain.entry('main').add(DEFAULT_CONFIG.ENTRY) + webpackChain.entry(EntryDefaultKey).add(DEFAULT_CONFIG.ENTRY) } } diff --git a/packages/rzpack/src/webpack/configs/extensions.ts b/packages/rzpack/src/common/configs/extensions.ts similarity index 100% rename from packages/rzpack/src/webpack/configs/extensions.ts rename to packages/rzpack/src/common/configs/extensions.ts diff --git a/packages/rzpack/src/webpack/configs/lazyCompilation.ts b/packages/rzpack/src/common/configs/lazyCompilation.ts similarity index 100% rename from packages/rzpack/src/webpack/configs/lazyCompilation.ts rename to packages/rzpack/src/common/configs/lazyCompilation.ts diff --git a/packages/rzpack/src/webpack/configs/output.ts b/packages/rzpack/src/common/configs/output.ts similarity index 82% rename from packages/rzpack/src/webpack/configs/output.ts rename to packages/rzpack/src/common/configs/output.ts index 8c2bf92..cba8c0c 100644 --- a/packages/rzpack/src/webpack/configs/output.ts +++ b/packages/rzpack/src/common/configs/output.ts @@ -1,13 +1,17 @@ import { pathResolve } from 'rzpack-utils' import type { Configuration } from 'webpack' import type WebpackChain from 'webpack-chain' +import { DEFAULT_CONFIG } from '../../constant' export type Output = Configuration['output'] | string const defaultFileName = 'assets/js/[name].[contenthash].js' const defaultChunkFilename = 'assets/js/[name].[contenthash].js' -export default (webpackChain: WebpackChain, output: Output = 'dist') => { +export default ( + webpackChain: WebpackChain, + output: Output = DEFAULT_CONFIG.OUTPUT, +) => { let configs: Output = { filename: defaultFileName, chunkFilename: defaultChunkFilename, @@ -19,7 +23,7 @@ export default (webpackChain: WebpackChain, output: Output = 'dist') => { configs = { ...configs, ...output, - path: pathResolve(output?.path, process.cwd()), + path: pathResolve(output?.path ?? DEFAULT_CONFIG.OUTPUT, process.cwd()), } } diff --git a/packages/rzpack/src/webpack/plugins/compression-webpack-plugin.ts b/packages/rzpack/src/common/plugins/compression-webpack-plugin.ts similarity index 100% rename from packages/rzpack/src/webpack/plugins/compression-webpack-plugin.ts rename to packages/rzpack/src/common/plugins/compression-webpack-plugin.ts diff --git a/packages/rzpack/src/webpack/plugins/eslint-webpack-plugin.ts b/packages/rzpack/src/common/plugins/eslint-webpack-plugin.ts similarity index 100% rename from packages/rzpack/src/webpack/plugins/eslint-webpack-plugin.ts rename to packages/rzpack/src/common/plugins/eslint-webpack-plugin.ts diff --git a/packages/rzpack/src/webpack/plugins/fork-ts-checker-webpack-plugin.ts b/packages/rzpack/src/common/plugins/fork-ts-checker-webpack-plugin.ts similarity index 100% rename from packages/rzpack/src/webpack/plugins/fork-ts-checker-webpack-plugin.ts rename to packages/rzpack/src/common/plugins/fork-ts-checker-webpack-plugin.ts diff --git a/packages/rzpack/src/webpack/plugins/friendly-errors-webpack-plugin.ts b/packages/rzpack/src/common/plugins/friendly-errors-webpack-plugin.ts similarity index 100% rename from packages/rzpack/src/webpack/plugins/friendly-errors-webpack-plugin.ts rename to packages/rzpack/src/common/plugins/friendly-errors-webpack-plugin.ts diff --git a/packages/rzpack/src/webpack/plugins/million-webpack-plugin.ts b/packages/rzpack/src/common/plugins/million-webpack-plugin.ts similarity index 100% rename from packages/rzpack/src/webpack/plugins/million-webpack-plugin.ts rename to packages/rzpack/src/common/plugins/million-webpack-plugin.ts diff --git a/packages/rzpack/src/webpack/plugins/unplugin-build-info.ts b/packages/rzpack/src/common/plugins/unplugin-build-info.ts similarity index 100% rename from packages/rzpack/src/webpack/plugins/unplugin-build-info.ts rename to packages/rzpack/src/common/plugins/unplugin-build-info.ts diff --git a/packages/rzpack/src/webpack/plugins/webpack-bundle-analyzer.ts b/packages/rzpack/src/common/plugins/webpack-bundle-analyzer.ts similarity index 100% rename from packages/rzpack/src/webpack/plugins/webpack-bundle-analyzer.ts rename to packages/rzpack/src/common/plugins/webpack-bundle-analyzer.ts diff --git a/packages/rzpack/src/constant.ts b/packages/rzpack/src/constant.ts index b9ea416..381ffef 100644 --- a/packages/rzpack/src/constant.ts +++ b/packages/rzpack/src/constant.ts @@ -1,3 +1,4 @@ +import { BUILDER as _BUILDER } from '.' import pkg from '../package.json' export const NAME = pkg.name @@ -8,4 +9,5 @@ export enum DEFAULT_CONFIG { HTML = './index.html', FAVICON = './favicon.ico', STATIC_DIR = 'public', + OUTPUT = 'dist', } diff --git a/packages/rzpack/src/ctx.ts b/packages/rzpack/src/ctx.ts index 830dcb3..09d66f2 100644 --- a/packages/rzpack/src/ctx.ts +++ b/packages/rzpack/src/ctx.ts @@ -9,8 +9,9 @@ import { import { bundleTsFile } from 'rzpack-utils' import type { Configuration } from 'webpack' import WebpackChain from 'webpack-chain' -import type { RzpackConfigs, RzpackWebpackChain, Yagt } from '.' +import { BUILDER, type RzpackChain, type RzpackConfigs, type Yagt } from '.' import { DEFAULT_CONFIG } from './constant' +import { loadRspackConfigs } from './rspack' import { loadWebpackConfigs } from './webpack' export interface RzpackContextConfigs extends Configuration { @@ -45,21 +46,22 @@ export const getBuildTmpFilePath = (filename: string) => { } export class RzpackContext { - public readonly webpackChain: WebpackChain + public readonly chain: WebpackChain public cache: boolean public bundleSize: boolean public bundleTime: boolean + public doctor: boolean public yagt: Yagt constructor() { - this.webpackChain = new WebpackChain() + this.chain = new WebpackChain() this.bundleSize = false this.bundleTime = false } set(key: string, value: any) { - this.webpackChain.set(key, value) + this.chain.set(key, value) } get(key: string) { - return this.webpackChain.get(key) + return this.chain.get(key) } loadConfigFile(configFilePath?: string): RzpackConfigs { let configFile: string @@ -92,19 +94,26 @@ export class RzpackContext { configs = bundleTsFile(configFile, tmpFilePath) } + if (!configs?.builder) { + configs.builder = BUILDER.WEBPACK + } + return configs } async configs(configs: RzpackConfigs) { if (typeof configs === 'object') { const { + builder, cache = true, - webpackChain: resolveWebpackChain, + rzpackChain: resolveRzpackChain, server, yagt, } = configs this.cache = cache this.yagt = yagt - await loadWebpackConfigs(this.webpackChain, configs) + const loadConfigs = + builder === BUILDER.WEBPACK ? loadWebpackConfigs : loadRspackConfigs + await loadConfigs(this.chain, configs) const { network, local, port } = await getNetwork( server?.port as unknown as number, ) @@ -115,19 +124,19 @@ export class RzpackContext { } if (server) { - this.webpackChain.merge({ devServer: { ...server, port } }) + this.chain.merge({ devServer: { ...server, port } }) } - resolveWebpackChain?.(this.webpackChain) - // 如果是一个函数则默认为webpackChain配置 + resolveRzpackChain?.(this.chain) + // 如果是一个函数则默认为rzpackChain配置 } else if (typeof configs === 'function') { - const resolveWebpackChain = configs as RzpackWebpackChain - resolveWebpackChain(this.webpackChain) + const resolveChain = configs as RzpackChain + resolveChain(this.chain) } else { console.log(logError('配置文件配置有误,请导出一个函数或对象')) } - this.webpackChain.mode(process.env.NODE_ENV as 'development' | 'production') + this.chain.mode(process.env.NODE_ENV as 'development' | 'production') } toConfig() { - return this.webpackChain.toConfig() as unknown as RzpackContextConfigs + return this.chain.toConfig() as unknown as RzpackContextConfigs } } diff --git a/packages/rzpack/src/index.ts b/packages/rzpack/src/index.ts index a6c2619..8ebf5ab 100644 --- a/packages/rzpack/src/index.ts +++ b/packages/rzpack/src/index.ts @@ -1,12 +1,13 @@ +import type { HtmlRspackPluginOptions } from '@rspack/core' import type HtmlWebpackPlugin from 'html-webpack-plugin' import type WebpackChain from 'webpack-chain' import type { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server' +import type { LazyCompilationOptions } from './common/configs/lazyCompilation' +import type { Output } from './common/configs/output' +import type { MillionOptions } from './common/plugins/million-webpack-plugin' +import type { BuildInfoWebpackPluginOptions } from './common/plugins/unplugin-build-info' import type { RzpackAssets } from './webpack/assets' -import type { LazyCompilationOptions } from './webpack/configs/lazyCompilation' -import type { Output } from './webpack/configs/output' -import type { MillionOptions } from './webpack/plugins/million-webpack-plugin' import type { ModuleFederationPluginOptions } from './webpack/plugins/module-federation-plugin' -import type { BuildInfoWebpackPluginOptions } from './webpack/plugins/unplugin-build-info' interface CLIOptions { '--'?: string[] @@ -27,6 +28,7 @@ export interface BuildOptions extends CLIOptions { outDir?: string bundleSize?: boolean bundleTime?: boolean + doctor?: boolean } export interface PreviewOptions @@ -34,9 +36,6 @@ export interface PreviewOptions outDir?: string } -// eslint-disable-next-line no-unused-vars -export type RzpackWebpackChain = (w: WebpackChain) => WebpackChain - export interface LessVars { // 全局变量(直接定义的变量优先级高于变量文件) vars?: Record @@ -52,6 +51,8 @@ export interface Yagt { } export interface RzpackConfigs { + // 打包器 + builder?: BUILDER // antd主题变量设置 antdTheme?: LessVars // less全局变量设置 @@ -60,14 +61,14 @@ export interface RzpackConfigs { assets?: RzpackAssets // 是否在控制台打印编译信息 buildInfo?: boolean | BuildInfoWebpackPluginOptions - // 是否使用webpack5缓存 + // 是否使用持久化缓存(目前只支持webpack) cache?: boolean // dll?: Array entry?: string | string[] | Record // 是否启用gzip gzip?: boolean // htmlPlugin插件设置 - html?: HtmlWebpackPlugin.Options + html?: HtmlWebpackPlugin.Options | HtmlRspackPluginOptions // 输出目录 output?: Output // 静态资源目录 @@ -78,8 +79,8 @@ export interface RzpackConfigs { lazyCompilation?: LazyCompilationOptions // 模块联邦 moduleFederation?: ModuleFederationPluginOptions - // 使用webpackChain重写webpack配置 - webpackChain?: RzpackWebpackChain + // 使用RspackChain重写配置 + rzpackChain?: RzpackChain // 可视化配置的代理,仅在开启可视化配置时才生效 proxyFile?: string // 是否开启React代码热更新 @@ -90,9 +91,16 @@ export interface RzpackConfigs { yagt?: Yagt } +export type RzpackChain = (w: WebpackChain) => WebpackChain + export const defineConfig = (configs: RzpackConfigs) => configs export enum JSX_TOOLS { ESBUILD = 'esbuild', SWC = 'swc', } + +export enum BUILDER { + WEBPACK = 'webpack', + RSPACK = 'rspack', +} diff --git a/packages/rzpack/src/rspack/assets/css.ts b/packages/rzpack/src/rspack/assets/css.ts new file mode 100644 index 0000000..85fff17 --- /dev/null +++ b/packages/rzpack/src/rspack/assets/css.ts @@ -0,0 +1,107 @@ +import { requireResolve } from 'rzpack-utils' +import type WebpackChain from 'webpack-chain' +import type { LessVars, RzpackConfigs } from '../../index' +import { applyPreCssLoader } from '../../webpack/assets/css' + +/** + * 应用公共css loader + * @param rule rule实例 + * @returns 返回rule实例 + */ +const applyCommonLoader = ( + rule: WebpackChain.Rule>, + cssScoped?: boolean, +) => { + const postcssPlugins: Array = [ + requireResolve('postcss-flexbugs-fixes'), + [ + requireResolve('postcss-preset-env'), + { + autoprefixer: { + flexbox: 'no-2009', + }, + stage: 3, + }, + ], + requireResolve('postcss-normalize'), + ] + rule + .set('type', cssScoped ? 'css' : 'css/auto') + .use('postcss-loader') + .loader(requireResolve('postcss-loader')) + .options({ + postcssOptions: { + ident: 'postcss', + config: false, + plugins: postcssPlugins, + }, + }) + .end() + + if (cssScoped) { + rule + .use('css-scoped-loader') + .loader(requireResolve('@renzp/css-scoped-loader')) + .end() + } + + return rule +} + +/** + * 创建css规则 + * @param baseRule rule实例 + * @param lang css语言 + * @param cssModule 是否为css module + * @param options loader的配置 + */ +export const createCssRule = ( + baseRule: WebpackChain.Rule, + lang: string, + cssScoped?: boolean, + antdTheme?: LessVars, + lessVars?: LessVars, +) => { + const regexps = { + css: [/\.css$/, /\.scoped\.css$/], + less: [/\.less$/, /\.scoped\.less$/], + } + const [cssTest, cssScopedTest] = regexps[lang] + + const rule = baseRule + .oneOf(lang) + .test(cssTest) + .exclude.add(cssScopedTest) + .end() + applyCommonLoader(rule) + + if (lang === 'less') { + applyPreCssLoader(rule, 'less-loader', antdTheme, lessVars) + } + + if (cssScoped) { + const scopedRule = baseRule.oneOf(`${lang}-scoped`).test(cssScopedTest) + applyCommonLoader(scopedRule, cssScoped) + applyPreCssLoader(scopedRule, 'less-loader', antdTheme, lessVars) + } +} + +export default ( + webpackChain: WebpackChain, + { antdTheme, lessVars, assets }: RzpackConfigs, +) => { + webpackChain.module.set('parser', { + 'css/auto': { + namedExports: false, + }, + }) + webpackChain.module.set('generator', { + 'css/auto': { + exportsConvention: 'camel-case', + }, + }) + + const baseRule = webpackChain.module.rule('css') + createCssRule(baseRule, 'css', assets?.cssScoped) + createCssRule(baseRule, 'less', assets?.cssScoped, antdTheme, lessVars) +} diff --git a/packages/rzpack/src/rspack/assets/font.ts b/packages/rzpack/src/rspack/assets/font.ts new file mode 100644 index 0000000..8f4a4d1 --- /dev/null +++ b/packages/rzpack/src/rspack/assets/font.ts @@ -0,0 +1,15 @@ +import type WebpackChain from 'webpack-chain' + +export default (webpackChain: WebpackChain) => { + webpackChain.module + .rule('fonts') + .test(/\.(woff|woff2|eot|ttf|otf)$/i) + .exclude.add(/node_modules/) + .end() + .set('type', 'asset') + .set('generator', { + asset: { + filename: 'assets/fonts/[name][ext]', + }, + }) +} diff --git a/packages/rzpack/src/rspack/assets/image.ts b/packages/rzpack/src/rspack/assets/image.ts new file mode 100644 index 0000000..43c35a9 --- /dev/null +++ b/packages/rzpack/src/rspack/assets/image.ts @@ -0,0 +1,102 @@ +import ImageMinimizerPlugin, { + type FilterFn, +} from 'image-minimizer-webpack-plugin' +import { requireResolve } from 'rzpack-utils' +import type WebpackChain from 'webpack-chain' + +export default (webpackChain: WebpackChain, imageMini?: boolean | FilterFn) => { + // 处理图片 + const rule = webpackChain.module + .rule('images') + .test(/\.(png|jpg|jpeg|gif|webp)$/i) + .exclude.add(/node_modules/) + .end() + .set('type', 'asset') + .parser({ + dataUrlCondition: { + maxSize: 80 * 1024, + }, + }) + .set('generator', { + filename: 'assets/images/[name].[hash:6][ext]', + }) + + if (imageMini) { + rule + .use('image-minimizer') + .loader(ImageMinimizerPlugin.loader) + .options([ + { + minimizer: { + filter: + typeof imageMini === 'boolean' + ? (source) => source.byteLength >= 1024 * 1024 + : imageMini, + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + ['gifsicle', { interlaced: true }], + ['jpegtran', { progressive: true }], + ['optipng', { optimizationLevel: 5 }], + [ + 'svgo', + { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + addAttributesToSVGElement: { + params: { + attributes: [ + { xmlns: 'http://www.w3.org/2000/svg' }, + ], + }, + }, + }, + }, + }, + ], + }, + ], + ], + }, + }, + }, + ]) + .end() + } + + // 处理svg可以直接导入为React组件 + webpackChain.module + .rule('svg') + .test(/\.svg$/) + .exclude.add(/node_modules/) + .end() + .oneOf('svg-img') + .set('resourceQuery', /url/) + .set('type', 'asset') + .parser({ + dataUrlCondition: { + maxSize: 80 * 1024, + }, + }) + .set('generator', { + filename: 'assets/images/[name].[hash:6][ext]', + }) + .end() + .oneOf('svg-icon') + .use('@svgr/webpack') + .loader(requireResolve('@svgr/webpack')) + .options({ + icon: true, + svgAttributes: { + fill: 'currentColor', + }, + svgoConfig: { + plugins: [{ name: 'convertColors', params: { currentColor: true } }], + }, + }) + .end() +} diff --git a/packages/rzpack/src/rspack/assets/index.ts b/packages/rzpack/src/rspack/assets/index.ts new file mode 100644 index 0000000..a04faf3 --- /dev/null +++ b/packages/rzpack/src/rspack/assets/index.ts @@ -0,0 +1,13 @@ +import type WebpackChain from 'webpack-chain' +import type { RzpackConfigs } from '../..' +import css from './css' +import font from './font' +import image from './image' +import jsx from './jsx' + +export default (chain: WebpackChain, options: RzpackConfigs) => { + css(chain, options) + font(chain) + jsx(chain, options?.reactRefresh, options?.assets?.cssScoped) + image(chain, options?.assets?.imageMini) +} diff --git a/packages/rzpack/src/rspack/assets/jsx.ts b/packages/rzpack/src/rspack/assets/jsx.ts new file mode 100644 index 0000000..14e4ba7 --- /dev/null +++ b/packages/rzpack/src/rspack/assets/jsx.ts @@ -0,0 +1,48 @@ +import { requireResolve } from 'rzpack-utils' +import type WebpackChain from 'webpack-chain' + +export default ( + chain: WebpackChain, + reactRefresh?: boolean, + cssScoped?: boolean, +) => { + const isProduction = process.env.NODE_ENV === 'production' + + const rule = chain.module + .rule('swc') + .test(/\.[tj]sx?$/) + .exclude.add(/node_modules/) + .end() + .type('javascript/auto') + .use('swc') + .loader('builtin:swc-loader') + .options({ + jsc: { + parser: { + syntax: 'typescript', + dynamicImport: true, + tsx: true, + decorators: true, + }, + target: 'es2015', + externalHelpers: false, + transform: { + react: { + runtime: 'automatic', + development: !isProduction, + refresh: reactRefresh && !isProduction, + useBuiltins: true, + }, + }, + }, + minify: isProduction, + }) + .end() + + if (cssScoped) { + rule + .use('jsx-scoped-loader') + .loader(requireResolve('@renzp/jsx-scoped-loader')) + .end() + } +} diff --git a/packages/rzpack/src/rspack/build.ts b/packages/rzpack/src/rspack/build.ts new file mode 100644 index 0000000..8275a8b --- /dev/null +++ b/packages/rzpack/src/rspack/build.ts @@ -0,0 +1,126 @@ +import rspack from '@rspack/core' +import { green, logError, red, yellow } from 'rzpack-utils' +import { bold, cyan, lightBlue, lightYellow } from 'rzpack-utils' +import { rzpack } from '../cli' + +export default (isLog = true) => { + // 抽离公共部分 + rzpack.chain + // 抛出错误之后停止打包 + .bail(true) + .optimization.splitChunks({ + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'chunk-vendors', + priority: -10, + chunks: 'initial', + }, + common: { + name: 'chunk-common', + minChunks: 2, + priority: -20, + chunks: 'initial', + reuseExistingChunk: true, + }, + }, + }) + + // if (rzpack.cache) { + // rzpack.chain.cache({ + // type: 'filesystem', + // name: `${process.env.NODE_ENV}-cache`, + // version: createEnvHash(raw), + // cacheDirectory: getFileFullPath('./node_modules/.cache'), + // store: 'pack', + // buildDependencies: { + // config: [__filename], + // tsconfig: [ + // getFileFullPath('tsconfig.json'), + // getFileFullPath('jsconfig.json'), + // ].filter((f) => fileExists(f)), + // }, + // }) + // } + + const configs = rzpack.toConfig() + // @ts-ignore + const compiler = rspack(configs) + + compiler.hooks.failed.tap('rzpack build', (msg) => { + logError(msg.toString()) + process.exit(1) + }) + + compiler.run((_, stats) => { + if (stats?.hasErrors()) { + return false + } + + if (isLog) { + logBuildAssets(stats) + } + + compiler.close((err) => { + if (err) { + console.log(err) + } + }) + }) + + return new Promise((resolve) => + compiler.hooks.afterDone.tap('rzpack', (stats) => + resolve(!stats?.hasErrors?.()), + ), + ) +} + +const logBuildAssets = (stats) => { + const { time, assets, warnings, errors } = stats.toJson() + console.log(lightBlue(bold('Assets:'))) + + assets?.map((item) => { + const { + name, + cached, + size, + info: { immutable, minimized }, + // related, + } = item + + const status = `${cached ? 'cached ' : ''}${immutable ? 'immutable ' : ''}${ + minimized ? 'minimized' : '' + }`.trim() + + // let gzipInfo = '' + // if (related && Object.keys(related).length > 0) { + // const [gzip] = related + // const gzipStatus = `${gzip?.cached ? 'cached ' : ''}${ + // gzip?.info?.immutable ? 'immutable ' : '' + // }${gzip?.info?.minimized ? 'minimized' : ''}`.trim() + // gzipInfo = `\n${gray( + // `${gzip.name} ${(gzip?.size / 1024).toFixed(3)}KB ${ + // gzipStatus ? `[${gzipStatus}]` : '' + // }`, + // )}` + // } + + console.log( + `${cyan(`${name} `)} ${lightYellow( + bold(`${(size / 1024).toFixed(3)}KB`), + )} ${status ? lightBlue(`[${status}]`) : ''}`, + // gzipInfo, + ) + }) + + console.log( + `\n🚨 Has ${red(errors?.length)} errors, ${yellow( + warnings.length, + )} warnings.`, + ) + console.log( + `✨ Rzpack build by ${green('Rspack')} done in ${green( + (time / 1000).toPrecision(2), + )}s.`, + ) +} diff --git a/packages/rzpack/src/rspack/configs/index.ts b/packages/rzpack/src/rspack/configs/index.ts new file mode 100644 index 0000000..3737a32 --- /dev/null +++ b/packages/rzpack/src/rspack/configs/index.ts @@ -0,0 +1,22 @@ +import type WebpackChain from 'webpack-chain' +import type { RzpackConfigs } from '../..' +import loadAliasConfigs from '../../common/configs/alias' +import loadEntryConfigs from '../../common/configs/entry' +import loadExtensionsConfigs from '../../common/configs/extensions' +import loadLazyCompilationConfigs from '../../common/configs/lazyCompilation' +import loadOutputConfigs from '../../common/configs/output' +import { DEFAULT_CONFIG } from '../../constant' + +const loadBaseConfigs = ( + webpackChain: WebpackChain, + configs: RzpackConfigs, +) => { + const { entry = DEFAULT_CONFIG.ENTRY, output, lazyCompilation } = configs + loadAliasConfigs(webpackChain) + loadEntryConfigs(webpackChain, entry) + loadExtensionsConfigs(webpackChain) + loadOutputConfigs(webpackChain, output) + loadLazyCompilationConfigs(webpackChain, lazyCompilation) +} + +export default loadBaseConfigs diff --git a/packages/rzpack/src/rspack/index.ts b/packages/rzpack/src/rspack/index.ts new file mode 100644 index 0000000..24cd7f7 --- /dev/null +++ b/packages/rzpack/src/rspack/index.ts @@ -0,0 +1,20 @@ +import type WebpackChain from 'webpack-chain' +import type { RzpackConfigs } from '..' +import { DEFAULT_CONFIG } from '../constant' +import loadAssetsConfigs from './assets' +import loadBaseConfigs from './configs' +import loadPluginsConfigs from './plugins' + +export const loadRspackConfigs = async ( + chain: WebpackChain, + configs: RzpackConfigs, +) => { + const { publicPath = DEFAULT_CONFIG.STATIC_DIR } = configs + loadBaseConfigs(chain, configs) + loadAssetsConfigs(chain, { ...configs, publicPath }) + await loadPluginsConfigs(chain, { ...configs, publicPath }) +} + +export { default as runRspackBuild } from './build' +export { default as runRspackPreview } from './preview' +export { default as runRspackServer } from './server' diff --git a/packages/rzpack/src/rspack/plugins/copy-rspack-plugin.ts b/packages/rzpack/src/rspack/plugins/copy-rspack-plugin.ts new file mode 100644 index 0000000..3a4ec8f --- /dev/null +++ b/packages/rzpack/src/rspack/plugins/copy-rspack-plugin.ts @@ -0,0 +1,21 @@ +import { CopyRspackPlugin } from '@rspack/core' +import { pathResolve } from 'rzpack-utils' +import type WebpackChain from 'webpack-chain' +import { DEFAULT_CONFIG } from '../../constant' + +export default ( + webpackChain: WebpackChain, + staticDir: string = DEFAULT_CONFIG.STATIC_DIR, +) => { + webpackChain.plugin('copy-rspack-plugin').use(CopyRspackPlugin, [ + { + patterns: [ + { + from: pathResolve(staticDir, process.cwd()), + to: webpackChain.output.get('path'), + noErrorOnMissing: true, + }, + ], + }, + ]) +} diff --git a/packages/rzpack/src/rspack/plugins/define-plugin.ts b/packages/rzpack/src/rspack/plugins/define-plugin.ts new file mode 100644 index 0000000..3b81948 --- /dev/null +++ b/packages/rzpack/src/rspack/plugins/define-plugin.ts @@ -0,0 +1,9 @@ +import { DefinePlugin } from '@rspack/core' +import { resolveClientEnv } from 'rzpack-utils' +import type WebpackChain from 'webpack-chain' + +export default (webpackChain: WebpackChain) => { + webpackChain + .plugin('define-plugin') + .use(DefinePlugin, [resolveClientEnv() as any]) +} diff --git a/packages/rzpack/src/rspack/plugins/hmr-plugin.ts b/packages/rzpack/src/rspack/plugins/hmr-plugin.ts new file mode 100644 index 0000000..3f566e8 --- /dev/null +++ b/packages/rzpack/src/rspack/plugins/hmr-plugin.ts @@ -0,0 +1,7 @@ +import { HotModuleReplacementPlugin } from '@rspack/core' +import { resolveClientEnv } from 'rzpack-utils' +import type WebpackChain from 'webpack-chain' + +export default (webpackChain: WebpackChain) => { + webpackChain.plugin('hmr-plugin').use(HotModuleReplacementPlugin) +} diff --git a/packages/rzpack/src/rspack/plugins/html-rspack-plugin.ts b/packages/rzpack/src/rspack/plugins/html-rspack-plugin.ts new file mode 100644 index 0000000..9de7549 --- /dev/null +++ b/packages/rzpack/src/rspack/plugins/html-rspack-plugin.ts @@ -0,0 +1,31 @@ +import { HtmlRspackPlugin, type HtmlRspackPluginOptions } from '@rspack/core' +import { fileExists, getFileFullPath } from 'rzpack-utils' +import type WebpackChain from 'webpack-chain' +import { DEFAULT_CONFIG } from '../../constant' + +export default ( + webpackChain: WebpackChain, + options: HtmlRspackPluginOptions = {}, +) => { + const { + title = 'rzpack demo', + template = DEFAULT_CONFIG.HTML, + favicon = DEFAULT_CONFIG.FAVICON, + } = options + + let hasFavicon = true + if (typeof favicon === 'string') { + const faviconFullPath = getFileFullPath(favicon) + hasFavicon = !!faviconFullPath && fileExists(faviconFullPath) + } + + webpackChain.plugin('html-webpack-plugin').use(HtmlRspackPlugin, [ + { + title, + template, + favicon: hasFavicon ? favicon : undefined, + ...options, + }, + ]) + return webpackChain +} diff --git a/packages/rzpack/src/rspack/plugins/index.ts b/packages/rzpack/src/rspack/plugins/index.ts new file mode 100644 index 0000000..30c4254 --- /dev/null +++ b/packages/rzpack/src/rspack/plugins/index.ts @@ -0,0 +1,71 @@ +import type { HtmlRspackPluginOptions } from '@rspack/core' +import { fileExists, getFileFullPath } from 'rzpack-utils' +import type WebpackChain from 'webpack-chain' +import type { RzpackConfigs } from '../..' +import { rzpack } from '../../cli' +import compressionWebpackPlugin from '../../common/plugins/compression-webpack-plugin' +import eslintWebpackPlugin from '../../common/plugins/eslint-webpack-plugin' +import forkTsCheckerWebpackPlugin from '../../common/plugins/fork-ts-checker-webpack-plugin' +import friendlyErrorsWebpackPlugin from '../../common/plugins/friendly-errors-webpack-plugin' +import millionWebpackPlugin from '../../common/plugins/million-webpack-plugin' +import unpluginBuildInfo from '../../common/plugins/unplugin-build-info' +import webpackBundleAnalyzer from '../../common/plugins/webpack-bundle-analyzer' +import copyRspackPlugin from './copy-rspack-plugin' +import definePlugin from './define-plugin' +import HMRPlugin from './hmr-plugin' +import htmlRspackPlugin from './html-rspack-plugin' +import progressPlugin from './progress-plugin' +import rsdoctorPlugin from './rsdoctor-rspack-plugin' + +export default async (chain: WebpackChain, options: RzpackConfigs) => { + const isProduction = process.env.NODE_ENV === 'production' + htmlRspackPlugin(chain, options?.html as HtmlRspackPluginOptions) + friendlyErrorsWebpackPlugin(chain) + progressPlugin(chain) + definePlugin(chain) + const isEslint = + fileExists(getFileFullPath('.eslintrc')) || + fileExists(getFileFullPath('.eslintrc.js')) || + fileExists(getFileFullPath('.eslintrc.ts')) || + fileExists(getFileFullPath('.eslintrc.json')) + + if (isEslint) { + eslintWebpackPlugin(chain) + } + const isForkTs = fileExists(getFileFullPath('tsconfig.json')) + if (isForkTs) { + forkTsCheckerWebpackPlugin(chain) + } + // // if (options?.dll?.length > 0) { + // // await useDll(options.dll) + // // dllPlugin(webpackChain) + // // } + if (options?.buildInfo) { + unpluginBuildInfo(chain, options?.buildInfo) + } + + if (options?.moduleFederation) { + moduleFederationWebpackPlugin(webpackChain, options?.moduleFederation) + } + + if (isProduction) { + if (options?.gzip) { + compressionWebpackPlugin(chain) + } + if (options?.million) { + millionWebpackPlugin( + chain, + typeof options?.million === 'boolean' ? undefined : options?.million, + ) + } + copyRspackPlugin(chain, options.publicPath) + if (rzpack.bundleSize) { + webpackBundleAnalyzer(chain) + } + if (rzpack.doctor) { + rsdoctorPlugin(chain) + } + } else { + HMRPlugin(chain) + } +} diff --git a/packages/rzpack/src/rspack/plugins/module-federation-plugin.ts b/packages/rzpack/src/rspack/plugins/module-federation-plugin.ts new file mode 100644 index 0000000..339e0a6 --- /dev/null +++ b/packages/rzpack/src/rspack/plugins/module-federation-plugin.ts @@ -0,0 +1,56 @@ +import { MFLiveReloadPlugin } from '@module-federation/fmr' +import rspack, { type ModuleFederationPluginOptions } from '@rspack/core' +import type WebpackChain from 'webpack-chain' +import { + type ModuleFederationPluginOptions as Options, + getSharedConfigs, +} from '../../webpack/plugins/module-federation-plugin' + +const ModuleFederationPlugin = rspack.container.ModuleFederationPlugin + +export default (webpackChain: WebpackChain, options: Options) => { + const { + filename = 'remote.js', + shared, + exposes, + ...restOptions + } = options ?? {} + const providerDefaultOption = exposes ? { runtime: false } : {} + let sharedConfigs: Record + if (shared) { + if (Array.isArray(shared)) { + sharedConfigs = shared.reduce((prev, item) => { + return { + ...prev, + [item.name]: { + requiredVersion: item.requiredVersion, + singleton: true, + }, + } + }, {} as any) + } else { + sharedConfigs = getSharedConfigs(shared) + } + } + + webpackChain.plugin('module-federation-plugin').use(ModuleFederationPlugin, [ + { + filename, + shared: sharedConfigs, + exposes, + ...providerDefaultOption, + ...restOptions, + } as ModuleFederationPluginOptions, + ]) + + if (!!exposes && process.env.Node === 'development') { + // 模块联邦热更新 + webpackChain.plugin('MFLiveReloadPlugin').use(MFLiveReloadPlugin, [ + { + port: webpackChain.get('port'), + container: options.name, + standalone: false, + }, + ]) + } +} diff --git a/packages/rzpack/src/rspack/plugins/progress-plugin.ts b/packages/rzpack/src/rspack/plugins/progress-plugin.ts new file mode 100644 index 0000000..2499e7e --- /dev/null +++ b/packages/rzpack/src/rspack/plugins/progress-plugin.ts @@ -0,0 +1,9 @@ +import { ProgressPlugin } from '@rspack/core' +import type WebpackChain from 'webpack-chain' +import { NAME, VERSION } from '../../constant' + +export default (chain: WebpackChain) => { + chain + .plugin('progress-plugin') + .use(ProgressPlugin, [{ prefix: `${NAME} V${VERSION}` }]) +} diff --git a/packages/rzpack/src/rspack/plugins/rsdoctor-rspack-plugin.ts b/packages/rzpack/src/rspack/plugins/rsdoctor-rspack-plugin.ts new file mode 100644 index 0000000..04c4fbf --- /dev/null +++ b/packages/rzpack/src/rspack/plugins/rsdoctor-rspack-plugin.ts @@ -0,0 +1,6 @@ +import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin' +import type WebpackChain from 'webpack-chain' + +export default (chain: WebpackChain) => { + chain.plugin('rsdoctor-rspack-plugin').use(RsdoctorRspackPlugin) +} diff --git a/packages/rzpack/src/rspack/preview.ts b/packages/rzpack/src/rspack/preview.ts new file mode 100644 index 0000000..d9fa0d8 --- /dev/null +++ b/packages/rzpack/src/rspack/preview.ts @@ -0,0 +1,25 @@ +import compression from 'compression' +import polka from 'polka' +import { getNetwork } from 'rzpack-utils' +import { cyan } from 'rzpack-utils' +import sirv from 'sirv' + +export default async (outDir: string) => { + const assets = sirv(outDir, { + gzip: true, + }) + const { network, local, port } = await getNetwork(3000) + + polka() + .use(compression(), assets) + .listen(port, (err) => { + if (err) { + throw err + } + console.log( + 'App preview at: \n', + `- Local: ${cyan(`http://${local}:${port}`)}\n`, + `- Network: ${cyan(`http://${network}:${port}`)}\n\n`, + ) + }) +} diff --git a/packages/rzpack/src/rspack/server.ts b/packages/rzpack/src/rspack/server.ts new file mode 100644 index 0000000..66f5bf1 --- /dev/null +++ b/packages/rzpack/src/rspack/server.ts @@ -0,0 +1,106 @@ +import rspack, { HtmlRspackPlugin } from '@rspack/core' +import { type Configuration, RspackDevServer } from '@rspack/dev-server' +import runUI, { + DEFAULT_CONFIG_FILE, + PREFIX_URL, + validateConfigFile, +} from 'rzpack-ui' +import { cyan, logError, logWarning } from 'rzpack-utils' +import { rzpack } from '../cli' + +export default async (startUI: boolean, proxyFile: string) => { + rzpack.chain.devtool('cheap-module-source-map') + const { network, local, port, ...configs } = rzpack.toConfig() + + // @ts-ignore + const compiler = rspack(configs) + compiler.hooks.failed.tap('rzpack', (msg) => { + logError(msg.toString()) + process.exit(1) + }) + + compiler.hooks.done.tap('rzpack', (stats) => { + if (stats.hasErrors()) { + return false + } + + if (startUI) { + console.log( + 'Rzpack UI run at: \n', + `- Local: ${cyan(`http://${local}:${port}${PREFIX_URL}`)}\n`, + `- Network: ${cyan(`http://${network}:${port}${PREFIX_URL}`)}\n\n`, + ) + } + + console.log( + 'App run at: \n', + `- Local: ${cyan(`http://${local}:${port}`)}\n`, + `- Network: ${cyan(`http://${network}:${port}`)}\n\n`, + 'Note that the development build is not optimized.\n', + `To create a production build, run ${cyan('yarn build')}.\n`, + ) + }) + const clientOverlay = { + client: { + overlay: { + errors: true, + runtimeErrors: false, + warnings: false, + }, + }, + } + const devServerOptions: Configuration = { + port, + compress: true, + ...clientOverlay, + ...((configs as any).devServer ?? {}), + } + + if (startUI) { + if (devServerOptions.proxy) { + logWarning( + '检测到使用Rzpack UI的同时,在配置文件配置了接口代理。配置文件中的配置将失效,优先使用Rzpack UI的代理模式', + ) + } + // 开启可视化配置时配置文件中配置的proxy将无效 + devServerOptions.proxy = undefined + let proxyFilePath: string | undefined = proxyFile + if (proxyFilePath && !validateConfigFile(proxyFilePath)) { + logWarning( + `接口代理配置文件仅支持json文件,${proxyFilePath}文件格式不对,将使用默认配置文件: ${DEFAULT_CONFIG_FILE}`, + ) + proxyFilePath = undefined + } + + const htmlRspackPlugin = configs.plugins?.find( + (item) => item instanceof HtmlRspackPlugin, + ) as { + _options?: { title?: string } + } + devServerOptions.setupMiddlewares = (middlewares, devServer) => { + try { + runUI({ + app: devServer.app, + proxyFile: proxyFilePath, + appTitle: htmlRspackPlugin?._options?.title, + yagt: rzpack.yagt, + }) + } catch (error) { + console.log(error) + } + + return middlewares + } + } + + const server = new RspackDevServer(devServerOptions, compiler as any) + const signals = ['SIGINT', 'SIGTERM'] + signals.forEach((signal) => + process.on(signal, () => { + server.stopCallback(() => process.exit(0)) + // 不用等dev server停掉再关闭进程,否则会出现Ctrl+C要等一会才能停止 + process.exit(0) + }), + ) + server.start() +} diff --git a/packages/rzpack/src/webpack/assets/css.ts b/packages/rzpack/src/webpack/assets/css.ts index 98d6e52..0f1b7db 100644 --- a/packages/rzpack/src/webpack/assets/css.ts +++ b/packages/rzpack/src/webpack/assets/css.ts @@ -8,7 +8,6 @@ import { requireResolve, } from 'rzpack-utils' import type WebpackChain from 'webpack-chain' -import { rzpack } from '../../cli' import { getBuildTmpFilePath } from '../../ctx' import type { LessVars, RzpackConfigs } from '../../index' @@ -47,7 +46,7 @@ const applyCommonLoader = ( modules: { auto: true, localIdentName: '[local]--[hash:base64:10]', - exportLocalsConvention: 'camelCaseOnly', + exportLocalsConvention: 'camelCase', }, }) .end() @@ -73,7 +72,7 @@ const applyCommonLoader = ( } // antd5主题设置跳转转px的key -const skipToPxKeys = [ +export const skipToPxKeys = [ 'motionBase', 'motionUnit', 'opacityImage', @@ -170,7 +169,7 @@ const useLessVars = (lessVars: LessVars) => { * @param loader 预处理的loader * @param options loader参数 */ -const applyPreCssLoader = ( +export const applyPreCssLoader = ( rule: WebpackChain.Rule>, loader: string, antdTheme?: LessVars, diff --git a/packages/rzpack/src/webpack/assets/jsx/swc.ts b/packages/rzpack/src/webpack/assets/jsx/swc.ts index 46de4e8..0594a23 100644 --- a/packages/rzpack/src/webpack/assets/jsx/swc.ts +++ b/packages/rzpack/src/webpack/assets/jsx/swc.ts @@ -1,6 +1,5 @@ import { requireResolve } from 'rzpack-utils' import type WebpackChain from 'webpack-chain' -import { rzpack } from '../../../cli' export default (webpackChain: WebpackChain) => { return webpackChain.module diff --git a/packages/rzpack/src/webpack/build.ts b/packages/rzpack/src/webpack/build.ts index e859221..d8e2490 100644 --- a/packages/rzpack/src/webpack/build.ts +++ b/packages/rzpack/src/webpack/build.ts @@ -1,4 +1,4 @@ -import { getFileFullPath, logError } from 'rzpack-utils' +import { getFileFullPath, green, logError, red, yellow } from 'rzpack-utils' import { bold, createEnvHash, @@ -13,7 +13,7 @@ import { rzpack } from '../cli' export default (isLog = true) => { // 抽离公共部分 - rzpack.webpackChain + rzpack.chain // 抛出错误之后停止打包 .bail(true) .optimization.splitChunks({ @@ -36,7 +36,7 @@ export default (isLog = true) => { const raw = Object.keys(process.env).reduce((env) => env) if (rzpack.cache) { - rzpack.webpackChain.cache({ + rzpack.chain.cache({ type: 'filesystem', name: `${process.env.NODE_ENV}-cache`, version: createEnvHash(raw), @@ -51,8 +51,8 @@ export default (isLog = true) => { }, }) } - const configs = rzpack.toConfig() + const configs = rzpack.toConfig() const compiler = Webpack(configs) compiler.hooks.failed.tap('rzpack build', (msg) => { @@ -84,7 +84,7 @@ export default (isLog = true) => { } const logBuildAssets = (stats) => { - const { time, assets } = stats.toJson() + const { time, assets, errors, warnings } = stats.toJson() console.log(lightBlue(bold('Assets:'))) @@ -121,5 +121,14 @@ const logBuildAssets = (stats) => { ) }) - console.log(`\n✨ Done in ${(time / 1000).toPrecision(2)}s.`) + console.log( + `\n🚨 Has ${red(errors?.length)} errors, ${yellow( + warnings.length, + )} warnings.`, + ) + console.log( + `✨ Rzpack build by ${green('Webpack')} done in ${green( + (time / 1000).toPrecision(2), + )}s.`, + ) } diff --git a/packages/rzpack/src/webpack/configs/index.ts b/packages/rzpack/src/webpack/configs/index.ts index ffd49e5..f9c474a 100644 --- a/packages/rzpack/src/webpack/configs/index.ts +++ b/packages/rzpack/src/webpack/configs/index.ts @@ -1,11 +1,12 @@ import type WebpackChain from 'webpack-chain' import type { RzpackConfigs } from '../..' -import loadAliasConfigs from './alias' -import loadEntryConfigs from './entry' -import loadExtensionsConfigs from './extensions' -import loadLazyCompilationConfigs from './lazyCompilation' +import loadAliasConfigs from '../../common/configs/alias' +import loadEntryConfigs from '../../common/configs/entry' +import loadExtensionsConfigs from '../../common/configs/extensions' +import loadLazyCompilationConfigs from '../../common/configs/lazyCompilation' +import loadOutputConfigs from '../../common/configs/output' import loadMinimizerConfigs from './minimizer' -import loadOutputConfigs from './output' + const loadBaseConfigs = ( webpackChain: WebpackChain, configs: RzpackConfigs, diff --git a/packages/rzpack/src/webpack/plugins/index.ts b/packages/rzpack/src/webpack/plugins/index.ts index 9651568..6ddbada 100644 --- a/packages/rzpack/src/webpack/plugins/index.ts +++ b/packages/rzpack/src/webpack/plugins/index.ts @@ -2,27 +2,29 @@ import { fileExists, getFileFullPath } from 'rzpack-utils' import type WebpackChain from 'webpack-chain' import type { RzpackConfigs } from '../..' import { rzpack } from '../../cli' -import compressionWebpackPlugin from './compression-webpack-plugin' +import compressionWebpackPlugin from '../../common/plugins/compression-webpack-plugin' +import eslintWebpackPlugin from '../../common/plugins/eslint-webpack-plugin' +import forkTsCheckerWebpackPlugin from '../../common/plugins/fork-ts-checker-webpack-plugin' +import friendlyErrorsWebpackPlugin from '../../common/plugins/friendly-errors-webpack-plugin' +import millionWebpackPlugin from '../../common/plugins/million-webpack-plugin' +import unpluginBuildInfo from '../../common/plugins/unplugin-build-info' +// import dllPlugin, { useDll } from './dll-plugin' +import webpackBundleAnalyzer from '../../common/plugins/webpack-bundle-analyzer' import copyWebpackPlugin from './copy-webpack-plugin' import definePlugin from './define-plugin' -import eslintWebpackPlugin from './eslint-webpack-plugin' -import forkTsCheckerWebpackPlugin from './fork-ts-checker-webpack-plugin' -import friendlyErrorsWebpackPlugin from './friendly-errors-webpack-plugin' import htmlWebpackPlugin from './html-webpack-plugin' -import millionWebpackPlugin from './million-webpack-plugin' import miniCssExtractPlugin from './mini-css-extract-plugin' import moduleFederationWebpackPlugin from './module-federation-plugin' import reactRefreshWebpackPlugin from './react-refresh-webpack-plugin' +import rsdoctorWebpackPlugin from './rsdoctor-webpack-plugin' import speedMeasureWebpackPlugin from './speed-measure-webpack-plugin' -import unpluginBuildInfo from './unplugin-build-info' -// import dllPlugin, { useDll } from './dll-plugin' -import webpackBundleAnalyzer from './webpack-bundle-analyzer' import webpackbar from './webpackbar' export default async (webpackChain: WebpackChain, options: RzpackConfigs) => { + const isProduction = process.env.NODE_ENV === 'production' htmlWebpackPlugin(webpackChain, options?.html) - webpackbar(webpackChain) friendlyErrorsWebpackPlugin(webpackChain) + webpackbar(webpackChain) definePlugin(webpackChain) const isEslint = @@ -48,20 +50,20 @@ export default async (webpackChain: WebpackChain, options: RzpackConfigs) => { unpluginBuildInfo(webpackChain, options?.buildInfo) } - if (options?.gzip) { - compressionWebpackPlugin(webpackChain) - } if (options?.moduleFederation) { moduleFederationWebpackPlugin(webpackChain, options?.moduleFederation) } - if (options?.million) { - millionWebpackPlugin( - webpackChain, - typeof options?.million === 'boolean' ? undefined : options?.million, - ) - } - if (process.env.NODE_ENV === 'production') { + if (isProduction) { + if (options?.million) { + millionWebpackPlugin( + webpackChain, + typeof options?.million === 'boolean' ? undefined : options?.million, + ) + } + if (options?.gzip) { + compressionWebpackPlugin(webpackChain) + } copyWebpackPlugin(webpackChain, options.publicPath) miniCssExtractPlugin(webpackChain) if (rzpack.bundleSize) { @@ -70,10 +72,13 @@ export default async (webpackChain: WebpackChain, options: RzpackConfigs) => { if (rzpack.bundleTime) { speedMeasureWebpackPlugin(webpackChain) } - } - - const refresh = options?.reactRefresh ?? true - if (process.env.NODE_ENV === 'development' && refresh) { - reactRefreshWebpackPlugin(webpackChain) + if (rzpack.doctor) { + rsdoctorWebpackPlugin(webpackChain) + } + } else { + const refresh = options?.reactRefresh ?? true + if (refresh) { + reactRefreshWebpackPlugin(webpackChain) + } } } diff --git a/packages/rzpack/src/webpack/plugins/module-federation-plugin.ts b/packages/rzpack/src/webpack/plugins/module-federation-plugin.ts index dd8fb85..baecd91 100644 --- a/packages/rzpack/src/webpack/plugins/module-federation-plugin.ts +++ b/packages/rzpack/src/webpack/plugins/module-federation-plugin.ts @@ -1,4 +1,5 @@ import { MFLiveReloadPlugin } from '@module-federation/fmr' +import type { Exposes, Remotes } from '@rspack/core' import { fileExists, getFileFullPath } from 'rzpack-utils' import type WebpackChain from 'webpack-chain' import ModuleFederationPlugin from 'webpack/lib/container/ModuleFederationPlugin' @@ -32,16 +33,16 @@ export interface ModuleFederationPluginOptions { // 要共享的依赖 shared?: ModuleFederationShared[] | ModuleFederationSharedAuto // 模块暴露的内容 - exposes?: boolean | Record + exposes?: Exposes // 模块引入的内容 - remotes?: Record + remotes?: Remotes } /** * 获取共享依赖配置 * @param shared 共享依赖 * @returns 返回处理后的共享依赖配置 */ -const getSharedConfigs = (shared: ModuleFederationSharedAuto) => { +export const getSharedConfigs = (shared: ModuleFederationSharedAuto) => { let sharedRecord: Record const pkgFilePath = getFileFullPath(shared?.depsPackagePath ?? 'package.json') diff --git a/packages/rzpack/src/webpack/plugins/rsdoctor-webpack-plugin.ts b/packages/rzpack/src/webpack/plugins/rsdoctor-webpack-plugin.ts new file mode 100644 index 0000000..e02eca5 --- /dev/null +++ b/packages/rzpack/src/webpack/plugins/rsdoctor-webpack-plugin.ts @@ -0,0 +1,6 @@ +import { RsdoctorWebpackPlugin } from '@rsdoctor/webpack-plugin' +import type WebpackChain from 'webpack-chain' + +export default (chain: WebpackChain) => { + chain.plugin('rsdoctor-webpack-plugin').use(RsdoctorWebpackPlugin) +} diff --git a/packages/rzpack/src/webpack/plugins/webpackbar.ts b/packages/rzpack/src/webpack/plugins/webpackbar.ts index f8f5afb..aa303c6 100644 --- a/packages/rzpack/src/webpack/plugins/webpackbar.ts +++ b/packages/rzpack/src/webpack/plugins/webpackbar.ts @@ -6,5 +6,4 @@ export default (webpackChain: WebpackChain) => { webpackChain .plugin('webpackbar') .use(Webpackbar, [{ name: `${NAME} V${VERSION}`, color: 'blue' }]) - return webpackChain } diff --git a/packages/rzpack/src/webpack/server.ts b/packages/rzpack/src/webpack/server.ts index 8002b79..8e2cc35 100644 --- a/packages/rzpack/src/webpack/server.ts +++ b/packages/rzpack/src/webpack/server.ts @@ -10,7 +10,7 @@ import WebpackDevServer, { type Configuration } from 'webpack-dev-server' import { rzpack } from '../cli' export default async (startUI: boolean, proxyFile: string) => { - rzpack.webpackChain.devtool('cheap-module-source-map') + rzpack.chain.devtool('cheap-module-source-map') const { network, local, port, ...webpackConfigs } = rzpack.toConfig() const compiler = Webpack(webpackConfigs) diff --git a/playground/index.html b/playground/index.html index eb27768..1920825 100644 --- a/playground/index.html +++ b/playground/index.html @@ -5,6 +5,7 @@ <%= htmlWebpackPlugin.options.title %> +
diff --git a/playground/package.json b/playground/package.json index aea4ea0..846dd21 100644 --- a/playground/package.json +++ b/playground/package.json @@ -5,6 +5,9 @@ "scripts": { "dev": "rzpack", "build": "rzpack build", + "build:time": "rzpack build --bundle-time", + "build:size": "rzpack build --bundle-size", + "build:doctor": "rzpack build --doctor", "preview": "rzpack preview" }, "author": "renzp94", diff --git a/playground/public/logo.png b/playground/public/logo.png new file mode 100644 index 0000000..003039f Binary files /dev/null and b/playground/public/logo.png differ diff --git a/playground/rzpack.config.ts b/playground/rzpack.config.ts index 3ac208e..2c81286 100644 --- a/playground/rzpack.config.ts +++ b/playground/rzpack.config.ts @@ -1,6 +1,7 @@ -import { JSX_TOOLS, defineConfig } from 'rzpack' +import { BUILDER, JSX_TOOLS, defineConfig } from 'rzpack' export default defineConfig({ + builder: BUILDER.RSPACK, html: { title: 'rzpack-antd', }, @@ -12,7 +13,8 @@ export default defineConfig({ }, assets: { jsxTools: JSX_TOOLS.ESBUILD, + cssScoped: true, }, + gzip:true, buildInfo: true, - million: true, }) diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 1c95bf2..8eaa0a3 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -1,6 +1,12 @@ -import { Button, DatePicker, message, Space } from 'antd' -import React, { useState } from 'react' +import { Button, DatePicker, Space, message } from 'antd' +import React from 'react' +import { useState } from 'react' import './app.less' +import './test.scoped.less' +import styles from './test-module.module.less' +import logo from '@/assets/images/logo.png' +import HomeIcon from '@/assets/images/home.svg' +import home from '@/assets/images/home.svg?url' const App: React.FC = () => { const [count, setCount] = useState(0) @@ -21,11 +27,21 @@ const App: React.FC = () => { return (
+ + + -
{count}
+
+ {count} +