chore: refactor ui plugin bundler kit and support rsbuild (#7568)

#### What type of PR is this?

/area ui
/kind feature
/area plugin
/milestone 2.21.x

#### What this PR does / why we need it:

This PR adds Rsbuild preset configuration support and refactors the Vite preset configuration approach for `@halo-dev/ui-plugin-bundler-kit`.

- **Added Rsbuild support** - New `rsbuildConfig` function with pre-configured settings for Halo plugin development
- **Refactored Vite configuration** - Improved `viteConfig` function with better preset handling
- **Updated documentation** - Comprehensive README with usage examples and build tool comparison

example:

```typescript
// Vite
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default viteConfig({ vite: { /* custom config */ } });

// Rsbuild
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default rsbuildConfig({ rsbuild: { /* custom config */ } });
```

real-world example: https://github.com/guqing/plugin-metrics-graph/pull/5

For detailed configuration options and examples, please refer to the updated README.md

⚠️ `HaloUIPluginBundlerKit` function is now deprecated (still functional but marked for removal)

#### Which issue(s) this PR fixes:

Fixes #

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
重构 `@halo-dev/ui-plugin-bundler-kit`,以预配置的方式为插件提供 Vite 和 Rsbuild 的构建配置。
```
pull/7573/head
Ryan Wang 2025-06-18 18:38:57 +08:00 committed by GitHub
parent d5a411eb1f
commit 1e5958dd5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 2565 additions and 344 deletions

View File

@ -0,0 +1,333 @@
# @halo-dev/ui-plugin-bundler-kit
A frontend build toolkit for Halo plugin development, supporting both Vite and Rsbuild build systems.
## Introduction
`@halo-dev/ui-plugin-bundler-kit` is a frontend build configuration toolkit specifically designed for Halo plugin development. It provides pre-configured build settings to help developers quickly set up and build frontend interfaces for Halo plugins.
### Key Features
- 🚀 **Ready to Use** - Provides pre-configured Vite and Rsbuild build settings
- 📦 **Multi-Build Tool Support** - Supports both Vite and Rsbuild
- 🔧 **Flexible Configuration** - Supports custom build configurations
- 🎯 **Halo Optimized** - External dependencies and global variables optimized for Halo plugin development
- 📁 **Smart Output** - Automatically selects output directory based on environment
## Installation
```bash
# Using npm
npm install @halo-dev/ui-plugin-bundler-kit
# Using yarn
yarn add @halo-dev/ui-plugin-bundler-kit
# Using pnpm
pnpm add @halo-dev/ui-plugin-bundler-kit
```
### Additional Dependencies
**For Vite users**, you need to install Vite:
```bash
npm install vite
```
**For Rsbuild users**, you need to install Rsbuild:
```bash
npm install @rsbuild/core
```
## Usage
### Vite Configuration
Create or update `vite.config.ts` file in your project root:
```typescript
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default viteConfig({
vite: {
// Your custom Vite configuration
plugins: [
// Additional plugins (Vue plugin is already included)
],
// Other configurations...
},
});
```
> **Note**: Vue plugin is pre-configured, no need to add it manually.
### Rsbuild Configuration
Create or update `rsbuild.config.ts` file in your project root:
```typescript
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default rsbuildConfig({
rsbuild: {
// Your custom Rsbuild configuration
plugins: [
// Additional plugins (Vue plugin is already included)
],
// Other configurations...
},
});
```
> **Note**: Vue plugin is pre-configured, no need to add it manually.
### Legacy Configuration (Deprecated)
> ⚠️ **Note**: The `HaloUIPluginBundlerKit` function is deprecated. Please use `viteConfig` or `rsbuildConfig` instead.
```typescript
import { HaloUIPluginBundlerKit } from "@halo-dev/ui-plugin-bundler-kit";
export default {
plugins: [
HaloUIPluginBundlerKit({
// Configuration options
}),
],
};
```
## Configuration Options
### Vite Configuration Options
```typescript
interface ViteUserConfig {
/**
* Halo plugin manifest file path
* @default "../src/main/resources/plugin.yaml"
*/
manifestPath?: string;
/**
* Custom Vite configuration
*/
vite: UserConfig;
}
```
### Rsbuild Configuration Options
```typescript
interface RsBuildUserConfig {
/**
* Halo plugin manifest file path
* @default "../src/main/resources/plugin.yaml"
*/
manifestPath?: string;
/**
* Custom Rsbuild configuration
*/
rsbuild: RsbuildConfig;
}
```
## Advanced Configuration Examples
### Adding Path Aliases (Vite)
```typescript
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
import path from "path";
export default viteConfig({
vite: {
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"@components": path.resolve(__dirname, "src/components"),
},
},
},
});
```
### Adding Path Aliases (Rsbuild)
```typescript
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default rsbuildConfig({
rsbuild: {
source: {
alias: {
"@": "./src",
"@components": "./src/components",
},
},
},
});
```
### Adding Additional Vite Plugins
```typescript
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
import { defineConfig } from "vite";
import UnoCSS from "unocss/vite";
export default viteConfig({
vite: {
plugins: [
UnoCSS(), // Add UnoCSS plugin
],
},
});
```
### Adding Additional Rsbuild Plugins
```typescript
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
import { pluginSass } from "@rsbuild/plugin-sass";
export default rsbuildConfig({
rsbuild: {
plugins: [
pluginSass(), // Add Sass plugin
],
},
});
```
### Custom Plugin Manifest Path
```typescript
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default viteConfig({
manifestPath: "application/src/main/resources/plugin.yaml", // Custom manifest file path
vite: {
// Other configurations...
},
});
```
## Development Scripts
Recommended scripts to add to your `package.json`:
```json
{
"scripts": {
"dev": "vite dev --mode=development --watch",
"build": "vite build"
}
}
```
For Rsbuild:
```json
{
"scripts": {
"dev": "rsbuild dev --env-mode=development --watch",
"build": "rsbuild build"
}
}
```
## Build Output
> Relative to the root directory of the Halo plugin project
- **Development**: `build/resources/main/console`
- **Production**: `ui/build/dist`
> **Note**: The production build output directory of `HaloUIPluginBundlerKit` is still `src/main/resources/console` to ensure compatibility.
## Requirements
- **Node.js**: ^18.0.0 || >=20.0.0
- **Peer Dependencies**:
- `@rsbuild/core`: ^1.0.0 (when using Rsbuild)
- `@rsbuild/plugin-vue`: ^1.0.0 (when using Rsbuild)
- `@vitejs/plugin-vue`: ^4.0.0 || ^5.0.0 (when using Vite)
- `vite`: ^4.0.0 || ^5.0.0 || ^6.0.0 (when using Vite)
## Vite vs Rsbuild
Both Vite and Rsbuild are excellent build tools, but they have different strengths depending on your use case:
### When to Use Rsbuild
**Recommended for large-scale plugins**
- ✅ **Code Splitting Support** - Rsbuild provides excellent support for code splitting and lazy loading
- ✅ **Better Performance** - Generally faster build times and smaller bundle sizes for complex applications
- ✅ **Dynamic Imports** - Perfect for plugins with heavy frontend components
**Example with dynamic imports:**
```typescript
import { definePlugin } from '@halo-dev/console-shared';
import { defineAsyncComponent } from 'vue';
import { VLoading } from '@halo-dev/components';
export default definePlugin({
routes: [
{
parentName: 'Root',
route: {
path: 'demo',
name: 'DemoPage',
// Lazy load heavy components
component: defineAsyncComponent({
loader: () => import('./views/DemoPage.vue'),
loadingComponent: VLoading,
}),
},
},
],
extensionPoints: {},
});
```
### When to Use Vite
**Recommended for simple to medium-scale plugins**
- ✅ **Vue Ecosystem Friendly** - Better integration with Vue ecosystem tools and plugins
- ✅ **Rich Plugin Ecosystem** - Extensive collection of Vite plugins available
- ✅ **Simple Configuration** - Easier to configure for straightforward use cases
### Summary
| Feature | Vite | Rsbuild |
|---------|------|---------|
| Code Splitting | ❌ Limited | ✅ Excellent |
| Vue Ecosystem | ✅ Excellent | ✅ Good |
| Build Performance | ✅ Good | ✅ Excellent |
| Dev Experience | ✅ Excellent | ✅ Excellent |
| Plugin Ecosystem | ✅ Rich | ✅ Growing |
| Configuration | ✅ Simple | ⚖️ Moderate |
**Recommendation**: Use **Rsbuild** for complex plugins with large frontend codebases, and **Vite** for simpler plugins or when you need extensive Vue ecosystem integration.
## License
GPL-3.0
## Contributing
Issues and Pull Requests are welcome! Please check our [Contributing Guide](https://github.com/halo-dev/halo/blob/main/CONTRIBUTING.md) for more information.
## Related Links
- [Halo Website](https://www.halo.run/)
- [Halo Documentation](https://docs.halo.run/)
- [GitHub Repository](https://github.com/halo-dev/halo)
- [Plugin Development Guide](https://docs.halo.run/category/ui)

View File

@ -2,11 +2,6 @@ import { defineBuildConfig } from "unbuild";
export default defineBuildConfig({
entries: ["src/index"],
externals: ["vite"],
clean: true,
declaration: true,
rollup: {
emitCJS: true,
inlineDependencies: true,
},
});

View File

@ -12,15 +12,14 @@
},
"license": "GPL-3.0",
"author": "@halo-dev",
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"main": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
@ -30,14 +29,19 @@
"dev": "unbuild --stub",
"prepublishOnly": "pnpm run build"
},
"devDependencies": {
"dependencies": {
"@halo-dev/api-client": "workspace:*",
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"js-yaml": "^4.1.0",
"unbuild": "^0.7.6"
"unbuild": "^3.5.0"
},
"peerDependencies": {
"vite": "^4.0.0 || ^5.0.0"
"@rsbuild/core": "^1.0.0",
"@rsbuild/plugin-vue": "^1.0.0",
"@vitejs/plugin-vue": "^4.0.0 || ^5.0.0",
"vite": "^4.0.0 || ^5.0.0 || ^6.0.0"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"

View File

@ -0,0 +1,4 @@
const DEFAULT_OUT_DIR_DEV = "../build/resources/main/console";
const DEFAULT_OUT_DIR_PROD = "./build/dist";
export { DEFAULT_OUT_DIR_DEV, DEFAULT_OUT_DIR_PROD };

View File

@ -0,0 +1,16 @@
const GLOBALS = {
vue: "Vue",
"vue-router": "VueRouter",
"@vueuse/core": "VueUse",
"@vueuse/components": "VueUse",
"@vueuse/router": "VueUse",
"@halo-dev/console-shared": "HaloConsoleShared",
"@halo-dev/components": "HaloComponents",
"@halo-dev/api-client": "HaloApiClient",
"@halo-dev/richtext-editor": "RichTextEditor",
axios: "axios",
};
const EXTERNALS = Object.keys(GLOBALS) as string[];
export { GLOBALS, EXTERNALS };

View File

@ -0,0 +1,3 @@
const DEFAULT_MANIFEST_PATH = "../src/main/resources/plugin.yaml";
export { DEFAULT_MANIFEST_PATH };

View File

@ -1,91 +1,3 @@
import type { Plugin as HaloPlugin } from "@halo-dev/api-client";
import fs from "fs";
import yaml from "js-yaml";
import { Plugin } from "vite";
const DEFAULT_OUT_DIR_DEV = "../src/main/resources/console";
const DEFAULT_OUT_DIR_PROD = "../build/resources/main/console";
const DEFAULT_MANIFEST_PATH = "../src/main/resources/plugin.yaml";
interface HaloUIPluginBundlerKitOptions {
outDir?:
| string
| {
dev: string;
prod: string;
};
manifestPath?: string;
}
export function HaloUIPluginBundlerKit(
options: HaloUIPluginBundlerKitOptions = {}
): Plugin {
return {
name: "halo-ui-plugin-bundler-kit",
config(config, env) {
const isProduction = env.mode === "production";
let outDir = isProduction ? DEFAULT_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV;
if (options.outDir) {
if (typeof options.outDir === "string") {
outDir = options.outDir;
} else {
outDir = isProduction ? options.outDir.prod : options.outDir.dev;
}
}
const manifestPath = options.manifestPath || DEFAULT_MANIFEST_PATH;
const manifest = yaml.load(
fs.readFileSync(manifestPath, "utf8")
) as HaloPlugin;
return {
...config,
define: {
"process.env": process.env,
},
build: {
outDir,
emptyOutDir: true,
lib: {
entry: "src/index.ts",
name: manifest.metadata.name,
formats: ["iife"],
fileName: () => "main.js",
},
rollupOptions: {
external: [
"vue",
"vue-router",
"@vueuse/core",
"@vueuse/components",
"@vueuse/router",
"@halo-dev/shared",
"@halo-dev/components",
"@halo-dev/api-client",
"@halo-dev/richtext-editor",
"axios",
],
output: {
globals: {
vue: "Vue",
"vue-router": "VueRouter",
"@vueuse/core": "VueUse",
"@vueuse/components": "VueUse",
"@vueuse/router": "VueUse",
"@halo-dev/console-shared": "HaloConsoleShared",
"@halo-dev/components": "HaloComponents",
"@halo-dev/api-client": "HaloApiClient",
"@halo-dev/richtext-editor": "RichTextEditor",
axios: "axios",
},
extend: true,
},
},
},
};
},
};
}
export { HaloUIPluginBundlerKit } from "./legacy";
export { viteConfig } from "./vite";
export { rsbuildConfig } from "./rsbuild";

View File

@ -0,0 +1,69 @@
import { Plugin } from "vite";
import { EXTERNALS, GLOBALS } from "./constants/externals";
import { DEFAULT_MANIFEST_PATH } from "./constants/halo-plugin";
import { DEFAULT_OUT_DIR_DEV } from "./constants/build";
import { getHaloPluginManifest } from "./utils/halo-plugin";
const LEGACY_OUT_DIR_PROD = "../src/main/resources/console";
interface HaloUIPluginBundlerKitOptions {
outDir?:
| string
| {
dev: string;
prod: string;
};
manifestPath?: string;
}
/**
* @deprecated Use `viteConfig` or `rsbuildConfig` instead.
*/
export function HaloUIPluginBundlerKit(
options: HaloUIPluginBundlerKitOptions = {}
): Plugin {
return {
name: "halo-ui-plugin-bundler-kit",
config(config, env) {
const isProduction = env.mode === "production";
let outDir = isProduction ? LEGACY_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV;
if (options.outDir) {
if (typeof options.outDir === "string") {
outDir = options.outDir;
} else {
outDir = isProduction ? options.outDir.prod : options.outDir.dev;
}
}
const manifestPath = options.manifestPath || DEFAULT_MANIFEST_PATH;
const manifest = getHaloPluginManifest(manifestPath);
return {
...config,
define: {
"process.env": process.env,
},
build: {
outDir,
emptyOutDir: true,
lib: {
entry: "src/index.ts",
name: manifest.metadata.name,
formats: ["iife"],
fileName: () => "main.js",
},
rollupOptions: {
external: EXTERNALS,
output: {
globals: GLOBALS,
extend: true,
},
},
},
};
},
};
}

View File

@ -0,0 +1,139 @@
import {
defineConfig,
mergeRsbuildConfig,
type RsbuildConfig,
type RsbuildMode,
} from "@rsbuild/core";
import { getHaloPluginManifest } from "./utils/halo-plugin";
import { DEFAULT_OUT_DIR_DEV, DEFAULT_OUT_DIR_PROD } from "./constants/build";
import { pluginVue } from "@rsbuild/plugin-vue";
import { GLOBALS } from "./constants/externals";
import { DEFAULT_MANIFEST_PATH } from "./constants/halo-plugin";
export interface RsBuildUserConfig {
/**
* Halo plugin manifest path.
*
* @default "../src/main/resources/plugin.yaml"
*/
manifestPath?: string;
/**
* Custom Rsbuild config.
*/
rsbuild: RsbuildConfig;
}
function createRsbuildPresetsConfig(manifestPath: string) {
const manifest = getHaloPluginManifest(manifestPath);
return defineConfig(({ envMode }) => {
const isProduction = envMode === "production";
const outDir = isProduction ? DEFAULT_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV;
return {
mode: (envMode as RsbuildMode) || "production",
plugins: [pluginVue()],
source: {
entry: {
main: "./src/index.ts",
},
},
dev: {
hmr: false,
},
performance: {
chunkSplit: {
strategy: "custom",
},
},
tools: {
rspack: {
optimization: {
splitChunks: {
chunks: "async",
},
moduleIds: "named",
},
experiments: {
rspackFuture: {
bundlerInfo: {
force: false,
},
},
},
module: {
parser: {
javascript: {
importMeta: false,
},
},
},
output: {
publicPath: `/plugins/${manifest.metadata.name}/assets/console/`,
library: {
type: "window",
export: "default",
name: manifest.metadata.name,
},
globalObject: "window",
iife: true,
},
},
htmlPlugin: false,
},
output: {
distPath: {
root: outDir,
js: "",
css: "",
jsAsync: "chunks",
cssAsync: "chunks",
},
cleanDistPath: true,
filename: {
css: (pathData) => {
if (pathData.chunk?.name === "main") {
return "style.css";
}
return "[name].[contenthash:8].css";
},
js: (pathData) => {
if (pathData.chunk?.name === "main") {
return "main.js";
}
return "[name].[contenthash:8].js";
},
},
externals: GLOBALS,
},
};
});
}
/**
* Rsbuild config for Halo UI Plugin.
*
* @example
* ```ts
* import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
*
* export default rsbuildConfig({
* rsbuild: {
* // your custom rsbuild config
* },
* });
* ```
* @param config
* @returns
*/
export function rsbuildConfig(config?: RsBuildUserConfig) {
const presetsConfigFn = createRsbuildPresetsConfig(
config?.manifestPath || DEFAULT_MANIFEST_PATH
);
return defineConfig((env) => {
const presetsConfig = presetsConfigFn(env);
return mergeRsbuildConfig(presetsConfig, config?.rsbuild || {});
});
}

View File

@ -0,0 +1,11 @@
import fs from "fs";
import yaml from "js-yaml";
import type { Plugin as HaloPlugin } from "@halo-dev/api-client";
export function getHaloPluginManifest(manifestPath: string) {
const manifest = yaml.load(
fs.readFileSync(manifestPath, "utf8")
) as HaloPlugin;
return manifest;
}

View File

@ -0,0 +1,77 @@
import { defineConfig, mergeConfig, UserConfig } from "vite";
import Vue from "@vitejs/plugin-vue";
import { EXTERNALS, GLOBALS } from "./constants/externals";
import { DEFAULT_OUT_DIR_DEV, DEFAULT_OUT_DIR_PROD } from "./constants/build";
import { getHaloPluginManifest } from "./utils/halo-plugin";
import { DEFAULT_MANIFEST_PATH } from "./constants/halo-plugin";
export interface ViteUserConfig {
/**
* Halo plugin manifest path.
*
* @default "../src/main/resources/plugin.yaml"
*/
manifestPath?: string;
/**
* Custom Vite config.
*/
vite: UserConfig;
}
function createVitePresetsConfig(manifestPath: string) {
const manifest = getHaloPluginManifest(manifestPath);
return defineConfig(({ mode }) => {
const isProduction = mode === "production";
return {
mode: mode || "production",
plugins: [Vue()],
define: {
"process.env": process.env,
},
build: {
outDir: isProduction ? DEFAULT_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV,
emptyOutDir: true,
lib: {
entry: "src/index.ts",
name: manifest.metadata.name,
formats: ["iife"],
fileName: () => "main.js",
},
rollupOptions: {
external: EXTERNALS,
output: {
globals: GLOBALS,
extend: true,
},
},
},
};
});
}
/**
* Vite config for Halo UI Plugin.
*
* @example
* ```ts
* import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
*
* export default viteConfig({
* vite: {
* // your custom vite config
* },
* });
* ```
*/
export function viteConfig(config?: ViteUserConfig) {
const presetsConfigFn = createVitePresetsConfig(
config?.manifestPath || DEFAULT_MANIFEST_PATH
);
return defineConfig((env) => {
const presetsConfig = presetsConfigFn(env);
return mergeConfig(presetsConfig, config?.vite || {});
});
}

File diff suppressed because it is too large Load Diff