build: migrate frontend bundling from webpack to Rspack

This commit is contained in:
Darkatse 2026-06-12 02:21:37 +08:00
parent 451a0e7897
commit bfbb1bc5b7
15 changed files with 283 additions and 989 deletions

View file

@ -48,8 +48,8 @@ src/
├── tauri-main.js # 轻量引导入口
├── init.js # 启动编排器
├── lib.js # 库门面(ESM 导出)
├── dist/lib.core.bundle.js # webpack 构建的核心依赖包(启动必需)
├── dist/lib.optional.bundle.js # webpack 构建的可选依赖包(按需加载)
├── dist/lib.core.bundle.js # Rspack 构建的核心依赖包(启动必需)
├── dist/lib.optional.bundle.js # Rspack 构建的可选依赖包(按需加载)
└── tauri/main/
├── bootstrap.js # 组合根(composition root)
├── context.js # 兼容 shim(re-export `context/index`)
@ -75,7 +75,7 @@ src/
前置要求:
- Rust stable
- Node.js 18+
- Node.js 22.12+
- pnpm
- Tauri CLI
@ -91,7 +91,7 @@ pnpm install
```bash
pnpm run check # guardrails + host kernel 类型检查(推荐每次改动后先跑)
pnpm run web:build # 构建前端资源包(webpack)
pnpm run web:build # 构建前端资源包(Rspack)
pnpm run dev # 桌面开发模式(等价 tauri:dev)
pnpm run tauri:dev # 桌面开发模式
pnpm run tauri:build # 构建桌面发行包

View file

@ -46,8 +46,8 @@ src/
├── tauri-main.js # thin bootstrap entry
├── init.js # startup orchestrator
├── lib.js # library facade (ESM exports)
├── dist/lib.core.bundle.js # webpack-built core vendor bundle (startup-critical)
├── dist/lib.optional.bundle.js # webpack-built optional vendor bundle (on-demand)
├── dist/lib.core.bundle.js # Rspack-built core vendor bundle (startup-critical)
├── dist/lib.optional.bundle.js # Rspack-built optional vendor bundle (on-demand)
└── tauri/main/
├── bootstrap.js # composition root
├── context.js # compatibility shim (re-export `context/index`)
@ -73,7 +73,7 @@ src/
Prerequisites:
- Rust stable
- Node.js 18+
- Node.js 22.12+
- pnpm
- Tauri CLI
@ -89,7 +89,7 @@ Common commands:
```bash
pnpm run check # guardrails + host-kernel type checks (recommended)
pnpm run web:build # build frontend bundles (webpack)
pnpm run web:build # build frontend bundles (Rspack)
pnpm run dev # desktop dev mode (alias of tauri:dev)
pnpm run tauri:dev # desktop dev mode
pnpm run tauri:build # build desktop installers

View file

@ -148,7 +148,7 @@
### 6.1 产物与入口
- webpack 入口:`webpack.config.js`
- Rspack 入口:`rspack.config.js`
- `lib.core` → `src/lib-bundle-core.js` → `src/dist/lib.core.bundle.js`
- `lib.optional` → `src/lib-bundle-optional.js` → `src/dist/lib.optional.bundle.js`

View file

@ -30,7 +30,7 @@
- **框架**: 保留SillyTavern原有的前端代码
- **核心技术**: HTML, CSS, JavaScript (jQuery)
- **构建工具**: Webpack (用于库打包)
- **构建工具**: Rspack (用于前端资源打包)
## 2. 后端技术栈
@ -78,8 +78,7 @@
### 3.2 前端构建工具
- **Webpack**: 用于打包第三方库
- **Babel**: JavaScript转译
- **Rspack**: 用于打包第三方库与 TauriTavern 前端入口
### 3.3 前端与后端通信
@ -160,7 +159,7 @@
- **资源加载**: 优化资源加载顺序
- **库注入链路**: `lib.js` 静态导入 `src/dist/lib.core.bundle.js`;重库通过 `getHljs()/getReadability()` 动态加载 `src/dist/lib.optional.bundle.js`
- **延迟加载**: 非关键资源延迟加载
- **构建缓存**: webpack filesystem cache 提升增量构建速度
- **构建缓存**: Rspack persistent cache 提升增量构建速度
## 8. 兼容性和迁移

View file

@ -16,8 +16,8 @@
"check:contracts": "pnpm run test:contracts",
"test": "pnpm run test:contracts",
"test:contracts": "node --test --test-concurrency=1 \"tests/**/*.test.mjs\"",
"web:build": "webpack --config webpack.config.js",
"web:dev": "webpack --config webpack.config.js --watch",
"web:build": "rspack build --config rspack.config.js",
"web:dev": "rspack build --config rspack.config.js --watch",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"tauri:build:portable": "node scripts/build-portable.mjs",
@ -32,19 +32,19 @@
"fastools:run": "cargo run --manifest-path fastools/Cargo.toml"
},
"devDependencies": {
"@rspack/cli": "^2.0.8",
"@rspack/core": "^2.0.8",
"@tauri-apps/cli": "^2.10.0",
"@types/jquery": "^3.5.33",
"@types/jquery-cropper": "^1.0.4",
"@types/jquery.transit": "^0.9.33",
"@types/jqueryui": "^1.12.24",
"@types/lodash": "^4.17.24",
"@types/node": "^18.19.84",
"@types/node": "^20.19.43",
"@types/select2": "^4.0.63",
"@types/toastr": "^2.1.43",
"es-module-lexer": "^2.0.0",
"typescript": "^5.9.3",
"webpack": "^5.105.4",
"webpack-cli": "^5.1.4"
"typescript": "^5.9.3"
},
"dependencies": {
"@adobe/css-tools": "^4.4.4",
@ -70,6 +70,6 @@
"vue": "^3.5.33"
},
"engines": {
"node": ">= 20"
"node": "^20.19.0 || >=22.12.0"
}
}

1126
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import webpack from 'webpack';
import * as rspack from '@rspack/core';
// Get the directory name of the current module
const __filename = fileURLToPath(import.meta.url);
@ -11,7 +11,7 @@ const __dirname = path.dirname(__filename);
const cacheEnvironment = `${process.platform}-${process.arch}-node${process.versions.node.split('.')[0]}`;
const commonCacheInputs = [
'webpack.config.js',
'rspack.config.js',
'package.json',
'pnpm-lock.yaml',
];
@ -71,7 +71,7 @@ function buildCacheVersion(name, inputFiles) {
hash.update(`platform=${process.platform}\n`);
hash.update(`arch=${process.arch}\n`);
hash.update(`node=${process.versions.node}\n`);
hash.update(`webpack=${webpack.version}\n`);
hash.update(`rspack=${rspack.rspackVersion}\n`);
for (const file of inputFiles) {
hash.update(`file=${file}\n`);
@ -82,15 +82,14 @@ function buildCacheVersion(name, inputFiles) {
return hash.digest('hex');
}
function createFilesystemCache(name, inputFiles) {
function createPersistentCache(name, inputFiles) {
return {
type: 'filesystem',
name,
cacheDirectory: path.resolve(__dirname, '.cache/webpack', cacheEnvironment, name),
type: 'persistent',
version: buildCacheVersion(name, inputFiles),
buildDependencies: {
config: [__filename],
inputs: inputFiles.map(resolveRepoPath),
buildDependencies: commonCacheInputs.map(resolveRepoPath),
storage: {
type: 'filesystem',
directory: path.resolve(__dirname, '.cache/rspack', cacheEnvironment, name),
},
};
}
@ -128,8 +127,26 @@ const sharedPerformance = {
maxAssetSize: 5120000
};
const sharedStats = {
preset: 'normal',
assets: true,
chunks: true,
modules: true,
entrypoints: true,
timings: true,
builtAt: true,
logging: 'warn',
cachedAssets: false,
cachedModules: false,
chunkModules: false,
assetsSort: '!size',
modulesSort: '!size',
assetsSpace: 20,
modulesSpace: 20,
};
function createVueDefinePlugin() {
return new webpack.DefinePlugin({
return new rspack.DefinePlugin({
__VUE_OPTIONS_API__: JSON.stringify(true),
__VUE_PROD_DEVTOOLS__: JSON.stringify(false),
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: JSON.stringify(false),
@ -139,8 +156,9 @@ function createVueDefinePlugin() {
const coreConfig = {
name: 'vendor-libs',
mode: 'production',
bail: true,
target: ['web', 'es2020'],
cache: createFilesystemCache('vendor-libs', libraryCacheInputs),
cache: createPersistentCache('vendor-libs', libraryCacheInputs),
entry: {
'lib.core': './src/lib-bundle-core.js',
'lib.optional': './src/lib-bundle-optional.js',
@ -148,41 +166,40 @@ const coreConfig = {
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'src/dist'),
module: true,
library: {
type: 'module'
}
},
experiments: {
outputModule: true,
},
resolve: sharedResolve,
optimization: sharedOptimization,
performance: sharedPerformance,
stats: sharedStats,
};
const agentSystemConfig = {
name: 'agent-system',
dependencies: ['vendor-libs'],
mode: 'production',
bail: true,
target: ['web', 'es2020'],
cache: createFilesystemCache('agent-system', agentSystemCacheInputs),
cache: createPersistentCache('agent-system', agentSystemCacheInputs),
entry: {
index: './src/scripts/extensions/agent-system/src/index.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'src/scripts/extensions/agent-system/dist'),
module: true,
library: {
type: 'module'
},
clean: true,
},
experiments: {
outputModule: true,
},
resolve: sharedResolve,
optimization: sharedOptimization,
performance: sharedPerformance,
stats: sharedStats,
plugins: [
createVueDefinePlugin(),
],
@ -192,8 +209,9 @@ const tauriTavernSettingsConfig = {
name: 'tauritavern-settings',
dependencies: ['vendor-libs'],
mode: 'production',
bail: true,
target: ['web', 'es2020'],
cache: createFilesystemCache('tauritavern-settings', tauriSettingUiCacheInputs),
cache: createPersistentCache('tauritavern-settings', tauriSettingUiCacheInputs),
entry: {
settings: './src/scripts/tauri/setting/settings-app/index.js',
'dev-logs': './src/scripts/tauri/setting/dev-logs-app/index.js',
@ -202,17 +220,16 @@ const tauriTavernSettingsConfig = {
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'src/scripts/tauri/setting/dist'),
module: true,
library: {
type: 'module'
},
clean: true,
},
experiments: {
outputModule: true,
},
resolve: sharedResolve,
optimization: sharedOptimization,
performance: sharedPerformance,
stats: sharedStats,
plugins: [
createVueDefinePlugin(),
],

View file

@ -14,7 +14,7 @@ function printHelp() {
Options:
--target <triple> Rust target triple, e.g. x86_64-pc-windows-msvc
--output-dir <path> Portable output directory (default: release)
--skip-web-build Skip frontend webpack build
--skip-web-build Skip frontend bundle build
--help Show this help message
`);
}

View file

@ -1,7 +1,7 @@
/**
* Library module facade for TauriTavern.
*
* We load a small core webpack bundle (`src/dist/lib.core.bundle.js`) and
* We load a small core Rspack bundle (`src/dist/lib.core.bundle.js`) and
* re-export the libraries expected by SillyTavern frontend modules.
*
* Heavy / feature-specific libraries live in `src/dist/lib.optional.bundle.js`

View file

@ -64,7 +64,8 @@ async function loadRollbackScript() {
if (rollbackScriptOverride) {
return rollbackScriptOverride;
}
return import(/* webpackIgnore: true */ '/script.js');
// Rspack supports webpackIgnore and leaves this host runtime import native.
return import('/script.js' /* webpackIgnore: true */);
}
export function __setAgentRunRollbackScriptForTests(script) {

View file

@ -69,10 +69,11 @@ function assertRetryRuntime(runtime) {
}
async function defaultRetryRuntime() {
// Rspack supports webpackIgnore and leaves these host runtime imports native.
const [script, groupChats, router] = await Promise.all([
import(/* webpackIgnore: true */ '/script.js'),
import(/* webpackIgnore: true */ '/scripts/group-chats.js'),
import(/* webpackIgnore: true */ '/scripts/tauritavern/agent/agent-generation-router.js'),
import('/script.js' /* webpackIgnore: true */),
import('/scripts/group-chats.js' /* webpackIgnore: true */),
import('/scripts/tauritavern/agent/agent-generation-router.js' /* webpackIgnore: true */),
]);
return {
Generate: script.Generate,

View file

@ -1720,6 +1720,22 @@ test('Agent System profile panel does not statically bundle main app modules', a
assert.match(modelTargetConnectionSource, /requireSillyTavernContext/);
});
test('Agent run helpers keep main runtime imports native for the extension bundle', async () => {
const controllerSource = await readFile(path.join(
REPO_ROOT,
'src/scripts/tauritavern/agent/agent-run-controller.js',
), 'utf8');
const retrySource = await readFile(path.join(
REPO_ROOT,
'src/scripts/tauritavern/agent/agent-run-retry.js',
), 'utf8');
assert.match(controllerSource, /import\(['"]\/script\.js['"]\s*\/\*\s*webpackIgnore:\s*true\s*\*\/\)/);
assert.match(retrySource, /import\(['"]\/script\.js['"]\s*\/\*\s*webpackIgnore:\s*true\s*\*\/\)/);
assert.match(retrySource, /import\(['"]\/scripts\/group-chats\.js['"]\s*\/\*\s*webpackIgnore:\s*true\s*\*\/\)/);
assert.match(retrySource, /import\(['"]\/scripts\/tauritavern\/agent\/agent-generation-router\.js['"]\s*\/\*\s*webpackIgnore:\s*true\s*\*\/\)/);
});
test('Skill extension resolves active scoped sections from SillyTavern context', async () => {
const hostWindow = installWindow({});
hostWindow.SillyTavern = {

View file

@ -46,8 +46,8 @@ test('TauriTavern Dev Logs wrapper owns the host ABI boundary', async () => {
assert.doesNotMatch(source, /devlog_/);
});
test('webpack exposes a dedicated TauriTavern Dev Logs Vue entry', async () => {
const source = await readRepoFile('webpack.config.js');
test('Rspack exposes a dedicated TauriTavern Dev Logs Vue entry', async () => {
const source = await readRepoFile('rspack.config.js');
assert.match(source, /['"]dev-logs['"]:\s*['"]\.\/src\/scripts\/tauri\/setting\/dev-logs-app\/index\.js['"]/);
assert.match(source, /listJavaScriptFiles\(['"]src\/scripts\/tauri\/setting\/dev-logs-app['"]\)/);

View file

@ -51,13 +51,13 @@ test('TauriTavern Settings wallpaper options use the no-render background refres
assert.doesNotMatch(source, /getBackgrounds/);
});
test('webpack exposes a dedicated TauriTavern Settings Vue entry', async () => {
const source = await readRepoFile('webpack.config.js');
test('Rspack exposes a dedicated TauriTavern Settings Vue entry', async () => {
const source = await readRepoFile('rspack.config.js');
assert.match(source, /name:\s*['"]tauritavern-settings['"]/);
assert.match(source, /settings:\s*['"]\.\/src\/scripts\/tauri\/setting\/settings-app\/index\.js['"]/);
assert.match(source, /src\/scripts\/tauri\/setting\/dist/);
assert.match(source, /createFilesystemCache\(['"]tauritavern-settings['"],\s*tauriSettingUiCacheInputs\)/);
assert.match(source, /createPersistentCache\(['"]tauritavern-settings['"],\s*tauriSettingUiCacheInputs\)/);
assert.match(source, /createVueDefinePlugin\(\)/);
});

View file

@ -59,8 +59,8 @@ test('TauriTavern Sync listeners keep event contract while delegating progress U
assert.doesNotMatch(source, /from\s+['"]vue(?:\/|['"])/);
});
test('webpack exposes a dedicated TauriTavern Sync Vue entry', async () => {
const source = await readRepoFile('webpack.config.js');
test('Rspack exposes a dedicated TauriTavern Sync Vue entry', async () => {
const source = await readRepoFile('rspack.config.js');
assert.match(source, /sync:\s*['"]\.\/src\/scripts\/tauri\/setting\/sync-app\/index\.js['"]/);
assert.match(source, /listJavaScriptFiles\(['"]src\/scripts\/tauri\/setting\/sync-app['"]\)/);