@halo-dev/ui-plugin-bundler-kit 2.24.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # @halo-dev/ui-plugin-bundler-kit
2
2
 
3
- A frontend build toolkit for Halo plugin development, supporting both Vite and Rsbuild build systems.
3
+ A frontend build toolkit for Halo UI plugin development, supporting both Vite and Rsbuild build systems.
4
4
 
5
5
  ## Introduction
6
6
 
7
- `@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.
7
+ `@halo-dev/ui-plugin-bundler-kit` is a frontend build configuration toolkit specifically designed for Halo UI plugin development. It provides pre-configured build settings to help developers quickly set up and build frontend interfaces for Halo plugins and theme-provided UI plugins.
8
8
 
9
9
  ### Key Features
10
10
 
11
11
  - 🚀 **Ready to Use** - Provides pre-configured Vite and Rsbuild build settings
12
12
  - 📦 **Multi-Build Tool Support** - Supports both Vite and Rsbuild
13
13
  - 🔧 **Flexible Configuration** - Supports custom build configurations
14
- - 🎯 **Halo Optimized** - External dependencies and global variables optimized for Halo plugin development
14
+ - 🎯 **Halo Optimized** - External dependencies and global variables optimized for Halo UI plugin development
15
15
  - 📁 **Smart Output** - Automatically selects output directory based on environment
16
16
 
17
17
  ## Installation
@@ -45,12 +45,13 @@ npm install @rsbuild/core
45
45
 
46
46
  ### Vite Configuration
47
47
 
48
- Create or update `vite.config.ts` file in your project root:
48
+ Create or update `vite.config.ts` file in your UI plugin project root:
49
49
 
50
50
  ```typescript
51
51
  import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
52
52
 
53
53
  export default viteConfig({
54
+ // provider defaults to "plugin"
54
55
  vite: {
55
56
  // Your custom Vite configuration
56
57
  plugins: [
@@ -65,12 +66,13 @@ export default viteConfig({
65
66
 
66
67
  ### Rsbuild Configuration
67
68
 
68
- Create or update `rsbuild.config.ts` file in your project root:
69
+ Create or update `rsbuild.config.ts` file in your UI plugin project root:
69
70
 
70
71
  ```typescript
71
72
  import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
72
73
 
73
74
  export default rsbuildConfig({
75
+ // provider defaults to "plugin"
74
76
  rsbuild: {
75
77
  // Your custom Rsbuild configuration
76
78
  plugins: [
@@ -83,9 +85,46 @@ export default rsbuildConfig({
83
85
 
84
86
  > **Note**: Vue plugin is pre-configured, no need to add it manually.
85
87
 
88
+ ### Theme UI Plugin Configuration
89
+
90
+ For theme-provided Console/User Center UI plugins, place the frontend project under the theme's `ui-plugin/` directory:
91
+
92
+ ```text
93
+ theme-root/
94
+ ├── theme.yaml
95
+ └── ui-plugin/
96
+ ├── package.json
97
+ ├── src/index.ts
98
+ └── vite.config.ts
99
+ ```
100
+
101
+ Vite:
102
+
103
+ ```typescript
104
+ import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
105
+
106
+ export default viteConfig({
107
+ provider: "theme",
108
+ vite: {},
109
+ });
110
+ ```
111
+
112
+ Rsbuild:
113
+
114
+ ```typescript
115
+ import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
116
+
117
+ export default rsbuildConfig({
118
+ provider: "theme",
119
+ rsbuild: {},
120
+ });
121
+ ```
122
+
123
+ The theme provider reads `../theme.yaml`, outputs to `dist`, registers the module as `theme:{metadata.name}`, and configures assets for `/themes/{metadata.name}/ui-plugin/assets/`. Halo reads only `ui-plugin/dist/**` from the theme package.
124
+
86
125
  ### Legacy Configuration (Deprecated)
87
126
 
88
- > ⚠️ **Note**: The `HaloUIPluginBundlerKit` function is deprecated. Please use `viteConfig` or `rsbuildConfig` instead.
127
+ > ⚠️ **Note**: The `HaloUIPluginBundlerKit` function is deprecated. Please use `viteConfig` or `rsbuildConfig` instead. It does not support `provider: "theme"`.
89
128
 
90
129
  ```typescript
91
130
  import { HaloUIPluginBundlerKit } from "@halo-dev/ui-plugin-bundler-kit";
@@ -106,8 +145,14 @@ export default {
106
145
  ```typescript
107
146
  interface ViteUserConfig {
108
147
  /**
109
- * Halo plugin manifest file path
110
- * @default "../src/main/resources/plugin.yaml"
148
+ * UI plugin provider type
149
+ * @default "plugin"
150
+ */
151
+ provider?: "plugin" | "theme";
152
+
153
+ /**
154
+ * Halo plugin or theme manifest file path
155
+ * @default "../src/main/resources/plugin.yaml" for plugins, "../theme.yaml" for themes
111
156
  */
112
157
  manifestPath?: string;
113
158
 
@@ -123,8 +168,14 @@ interface ViteUserConfig {
123
168
  ```typescript
124
169
  interface RsBuildUserConfig {
125
170
  /**
126
- * Halo plugin manifest file path
127
- * @default "../src/main/resources/plugin.yaml"
171
+ * UI plugin provider type
172
+ * @default "plugin"
173
+ */
174
+ provider?: "plugin" | "theme";
175
+
176
+ /**
177
+ * Halo plugin or theme manifest file path
178
+ * @default "../src/main/resources/plugin.yaml" for plugins, "../theme.yaml" for themes
128
179
  */
129
180
  manifestPath?: string;
130
181
 
@@ -216,6 +267,20 @@ export default viteConfig({
216
267
  });
217
268
  ```
218
269
 
270
+ ### Custom Theme Manifest Path
271
+
272
+ ```typescript
273
+ import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
274
+
275
+ export default viteConfig({
276
+ provider: "theme",
277
+ manifestPath: "../custom-theme.yaml",
278
+ vite: {
279
+ // Other configurations...
280
+ },
281
+ });
282
+ ```
283
+
219
284
  ## Development Scripts
220
285
 
221
286
  Recommended scripts to add to your `package.json`:
@@ -242,10 +307,17 @@ For Rsbuild:
242
307
 
243
308
  ## Build Output
244
309
 
245
- > Relative to the root directory of the Halo plugin project
310
+ > Relative to the UI plugin project root
311
+
312
+ Plugin provider:
313
+
314
+ - **Development**: `../build/resources/main/ui` or `../build/resources/main/console`
315
+ - **Production**: `./build/dist`
316
+
317
+ Theme provider:
246
318
 
247
- - **Development**: `build/resources/main/console`
248
- - **Production**: `ui/build/dist`
319
+ - **Development**: `dist`
320
+ - **Production**: `dist`
249
321
 
250
322
  > **Note**: The production build output directory of `HaloUIPluginBundlerKit` is still `src/main/resources/console` to ensure compatibility.
251
323
 
package/dist/index.d.mts CHANGED
@@ -17,9 +17,15 @@ declare function HaloUIPluginBundlerKit(options?: HaloUIPluginBundlerKitOptions)
17
17
  //#region src/rsbuild.d.ts
18
18
  interface RsBuildUserConfig {
19
19
  /**
20
- * Halo plugin manifest path.
20
+ * UI plugin provider type.
21
21
  *
22
- * @default "../src/main/resources/plugin.yaml"
22
+ * @default "plugin"
23
+ */
24
+ provider?: "plugin" | "theme";
25
+ /**
26
+ * Halo plugin or theme manifest path.
27
+ *
28
+ * @default "../src/main/resources/plugin.yaml" for plugins, "../theme.yaml" for themes
23
29
  */
24
30
  manifestPath?: string;
25
31
  /**
@@ -48,9 +54,15 @@ declare function rsbuildConfig(config?: RsBuildUserConfig): (env: ConfigParams)
48
54
  //#region src/vite.d.ts
49
55
  interface ViteUserConfig {
50
56
  /**
51
- * Halo plugin manifest path.
57
+ * UI plugin provider type.
58
+ *
59
+ * @default "plugin"
60
+ */
61
+ provider?: "plugin" | "theme";
62
+ /**
63
+ * Halo plugin or theme manifest path.
52
64
  *
53
- * @default "../src/main/resources/plugin.yaml"
65
+ * @default "../src/main/resources/plugin.yaml" for plugins, "../theme.yaml" for themes
54
66
  */
55
67
  manifestPath?: string;
56
68
  /**
package/dist/index.mjs CHANGED
@@ -1,12 +1,18 @@
1
1
  import fs from "node:fs";
2
2
  import yaml from "js-yaml";
3
+ import { gte, minVersion } from "semver";
3
4
  import { defineConfig, mergeRsbuildConfig } from "@rsbuild/core";
4
5
  import { pluginVue } from "@rsbuild/plugin-vue";
5
6
  import Vue from "@vitejs/plugin-vue";
6
7
  import { defineConfig as defineConfig$1, mergeConfig } from "vite";
7
8
  //#region src/constants/build.ts
8
9
  const DEFAULT_OUT_DIR_DEV = "../build/resources/main/console";
10
+ const DEFAULT_OUT_DIR_DEV_BASE = "../build/resources/main";
9
11
  const DEFAULT_OUT_DIR_PROD = "./build/dist";
12
+ const DEFAULT_THEME_OUT_DIR = "dist";
13
+ function getDefaultOutDirDev(bundleLocation) {
14
+ return `${DEFAULT_OUT_DIR_DEV_BASE}/${bundleLocation}`;
15
+ }
10
16
  //#endregion
11
17
  //#region src/constants/externals.ts
12
18
  const GLOBALS = {
@@ -20,12 +26,50 @@ const GLOBALS = {
20
26
  "@halo-dev/components": "HaloComponents",
21
27
  "@halo-dev/api-client": "HaloApiClient",
22
28
  "@halo-dev/richtext-editor": "RichTextEditor",
29
+ "@formkit/vue": "FormKitVue",
23
30
  axios: "axios"
24
31
  };
25
32
  const EXTERNALS = Object.keys(GLOBALS);
26
33
  //#endregion
34
+ //#region src/constants/halo-plugin.ts
35
+ const DEFAULT_PLUGIN_MANIFEST_PATH = "../src/main/resources/plugin.yaml";
36
+ const DEFAULT_THEME_MANIFEST_PATH = "../theme.yaml";
37
+ //#endregion
27
38
  //#region src/utils/halo-plugin.ts
39
+ const UI_BUNDLE_MIN_HALO_VERSION = "2.25.0";
40
+ const UI_BUNDLE_LOCATION = "ui";
41
+ const CONSOLE_BUNDLE_LOCATION = "console";
42
+ const THEME_MODULE_NAME_PREFIX = "theme:";
28
43
  function getHaloPluginManifest(manifestPath) {
44
+ return readManifest(manifestPath);
45
+ }
46
+ function getHaloThemeManifest(manifestPath) {
47
+ return readManifest(manifestPath);
48
+ }
49
+ function getManifestName(manifest) {
50
+ return manifest.metadata.name;
51
+ }
52
+ function getHaloThemeModuleName(manifest) {
53
+ return `${THEME_MODULE_NAME_PREFIX}${getManifestName(manifest)}`;
54
+ }
55
+ function getHaloThemeAssetPublicPath(manifest) {
56
+ return `/themes/${getManifestName(manifest)}/ui-plugin/assets/`;
57
+ }
58
+ function getHaloPluginBundleLocation(manifest) {
59
+ const requiresMinVersion = getRequiresMinVersion(manifest.spec.requires);
60
+ return requiresMinVersion && gte(requiresMinVersion, UI_BUNDLE_MIN_HALO_VERSION) ? UI_BUNDLE_LOCATION : CONSOLE_BUNDLE_LOCATION;
61
+ }
62
+ function getRequiresMinVersion(requires) {
63
+ const normalizedRequires = requires?.trim();
64
+ if (!normalizedRequires) return;
65
+ try {
66
+ return minVersion(normalizedRequires);
67
+ } catch {
68
+ console.warn(`[ui-plugin-bundler-kit] Invalid semver range in plugin manifest "spec.requires": "${requires}". Falling back to "${CONSOLE_BUNDLE_LOCATION}" bundle location.`);
69
+ return;
70
+ }
71
+ }
72
+ function readManifest(manifestPath) {
29
73
  return yaml.load(fs.readFileSync(manifestPath, "utf8"));
30
74
  }
31
75
  //#endregion
@@ -69,10 +113,10 @@ function HaloUIPluginBundlerKit(options = {}) {
69
113
  }
70
114
  //#endregion
71
115
  //#region src/rsbuild.ts
72
- function createRsbuildPresetsConfig(manifestPath) {
73
- const manifest = getHaloPluginManifest(manifestPath);
116
+ function createRsbuildPresetsConfig(provider, manifestPath) {
117
+ const defaults = provider === "theme" ? getThemeProviderDefaults$1(manifestPath) : getPluginProviderDefaults$1(manifestPath);
74
118
  return defineConfig(({ envMode }) => {
75
- const outDir = envMode === "production" ? DEFAULT_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV;
119
+ const outDir = envMode === "production" ? defaults.outDir.prod : defaults.outDir.dev;
76
120
  return {
77
121
  mode: envMode || "production",
78
122
  plugins: [pluginVue()],
@@ -88,11 +132,11 @@ function createRsbuildPresetsConfig(manifestPath) {
88
132
  experiments: { rspackFuture: { bundlerInfo: { force: false } } },
89
133
  module: { parser: { javascript: { importMeta: false } } },
90
134
  output: {
91
- publicPath: `/plugins/${manifest.metadata.name}/assets/console/`,
135
+ publicPath: defaults.publicPath,
92
136
  library: {
93
137
  type: "window",
94
138
  export: "default",
95
- name: manifest.metadata.name
139
+ name: defaults.moduleName
96
140
  },
97
141
  globalObject: "window",
98
142
  iife: true
@@ -124,6 +168,36 @@ function createRsbuildPresetsConfig(manifestPath) {
124
168
  };
125
169
  });
126
170
  }
171
+ function getPluginProviderDefaults$1(manifestPath) {
172
+ const manifest = getHaloPluginManifest(manifestPath);
173
+ const bundleLocation = getHaloPluginBundleLocation(manifest);
174
+ return {
175
+ moduleName: getManifestName(manifest),
176
+ outDir: {
177
+ prod: DEFAULT_OUT_DIR_PROD,
178
+ dev: getDefaultOutDirDev(bundleLocation)
179
+ },
180
+ publicPath: `/plugins/${getManifestName(manifest)}/assets/${bundleLocation}/`
181
+ };
182
+ }
183
+ function getThemeProviderDefaults$1(manifestPath) {
184
+ const manifest = getHaloThemeManifest(manifestPath);
185
+ return {
186
+ moduleName: getHaloThemeModuleName(manifest),
187
+ outDir: {
188
+ prod: DEFAULT_THEME_OUT_DIR,
189
+ dev: DEFAULT_THEME_OUT_DIR
190
+ },
191
+ publicPath: getHaloThemeAssetPublicPath(manifest)
192
+ };
193
+ }
194
+ function getProvider$1(config) {
195
+ return config?.provider || "plugin";
196
+ }
197
+ function getManifestPath$1(provider, config) {
198
+ if (config?.manifestPath) return config.manifestPath;
199
+ return provider === "theme" ? DEFAULT_THEME_MANIFEST_PATH : DEFAULT_PLUGIN_MANIFEST_PATH;
200
+ }
127
201
  /**
128
202
  * Rsbuild config for Halo UI Plugin.
129
203
  *
@@ -141,27 +215,29 @@ function createRsbuildPresetsConfig(manifestPath) {
141
215
  * @returns
142
216
  */
143
217
  function rsbuildConfig(config) {
144
- const presetsConfigFn = createRsbuildPresetsConfig(config?.manifestPath || "../src/main/resources/plugin.yaml");
218
+ const provider = getProvider$1(config);
219
+ const presetsConfigFn = createRsbuildPresetsConfig(provider, getManifestPath$1(provider, config));
145
220
  return defineConfig((env) => {
146
221
  return mergeRsbuildConfig(presetsConfigFn(env), typeof config?.rsbuild === "function" ? config.rsbuild(env) : config?.rsbuild || {});
147
222
  });
148
223
  }
149
224
  //#endregion
150
225
  //#region src/vite.ts
151
- function createVitePresetsConfig(manifestPath) {
152
- const manifest = getHaloPluginManifest(manifestPath);
226
+ function createVitePresetsConfig(provider, manifestPath) {
227
+ const defaults = provider === "theme" ? getThemeProviderDefaults(manifestPath) : getPluginProviderDefaults(manifestPath);
153
228
  return defineConfig$1(({ mode }) => {
154
229
  const isProduction = mode === "production";
155
230
  return {
156
231
  mode: mode || "production",
232
+ base: defaults.base,
157
233
  plugins: [Vue()],
158
234
  define: { "process.env.NODE_ENV": "'production'" },
159
235
  build: {
160
- outDir: isProduction ? DEFAULT_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV,
236
+ outDir: isProduction ? defaults.outDir.prod : defaults.outDir.dev,
161
237
  emptyOutDir: true,
162
238
  lib: {
163
239
  entry: "src/index.ts",
164
- name: manifest.metadata.name,
240
+ name: defaults.moduleName,
165
241
  formats: ["iife"],
166
242
  fileName: () => "main.js",
167
243
  cssFileName: "style"
@@ -177,6 +253,36 @@ function createVitePresetsConfig(manifestPath) {
177
253
  };
178
254
  });
179
255
  }
256
+ function getPluginProviderDefaults(manifestPath) {
257
+ const manifest = getHaloPluginManifest(manifestPath);
258
+ const bundleLocation = getHaloPluginBundleLocation(manifest);
259
+ return {
260
+ moduleName: getManifestName(manifest),
261
+ outDir: {
262
+ prod: DEFAULT_OUT_DIR_PROD,
263
+ dev: getDefaultOutDirDev(bundleLocation)
264
+ },
265
+ base: void 0
266
+ };
267
+ }
268
+ function getThemeProviderDefaults(manifestPath) {
269
+ const manifest = getHaloThemeManifest(manifestPath);
270
+ return {
271
+ moduleName: getHaloThemeModuleName(manifest),
272
+ outDir: {
273
+ prod: DEFAULT_THEME_OUT_DIR,
274
+ dev: DEFAULT_THEME_OUT_DIR
275
+ },
276
+ base: getHaloThemeAssetPublicPath(manifest)
277
+ };
278
+ }
279
+ function getProvider(config) {
280
+ return config?.provider || "plugin";
281
+ }
282
+ function getManifestPath(provider, config) {
283
+ if (config?.manifestPath) return config.manifestPath;
284
+ return provider === "theme" ? DEFAULT_THEME_MANIFEST_PATH : DEFAULT_PLUGIN_MANIFEST_PATH;
285
+ }
180
286
  /**
181
287
  * Vite config for Halo UI Plugin.
182
288
  *
@@ -192,7 +298,8 @@ function createVitePresetsConfig(manifestPath) {
192
298
  * ```
193
299
  */
194
300
  function viteConfig(config) {
195
- const presetsConfigFn = createVitePresetsConfig(config?.manifestPath || "../src/main/resources/plugin.yaml");
301
+ const provider = getProvider(config);
302
+ const presetsConfigFn = createVitePresetsConfig(provider, getManifestPath(provider, config));
196
303
  return defineConfig$1((env) => {
197
304
  return mergeConfig(presetsConfigFn(env), typeof config?.vite === "function" ? config.vite(env) : config?.vite || {});
198
305
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halo-dev/ui-plugin-bundler-kit",
3
- "version": "2.24.0",
3
+ "version": "2.25.0",
4
4
  "homepage": "https://github.com/halo-dev/halo/tree/main/ui/packages/ui-plugin-bundler-kit#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/halo-dev/halo/issues"
@@ -18,12 +18,20 @@
18
18
  ".": "./dist/index.mjs",
19
19
  "./package.json": "./package.json"
20
20
  },
21
+ "scripts": {
22
+ "build": "vp pack",
23
+ "dev": "vp pack --watch",
24
+ "test:unit": "vp test --run",
25
+ "prepublishOnly": "vp run build"
26
+ },
21
27
  "dependencies": {
28
+ "@halo-dev/api-client": "workspace:*",
22
29
  "js-yaml": "^4.1.1",
23
- "@halo-dev/api-client": "2.24.0"
30
+ "semver": "^7.7.4"
24
31
  },
25
32
  "devDependencies": {
26
- "@types/js-yaml": "^4.0.9"
33
+ "@types/js-yaml": "^4.0.9",
34
+ "@types/semver": "^7.7.1"
27
35
  },
28
36
  "peerDependencies": {
29
37
  "@rsbuild/core": "^1.0.0 || ^2.0.0",
@@ -33,9 +41,5 @@
33
41
  },
34
42
  "engines": {
35
43
  "node": "^18.0.0 || >=20.0.0"
36
- },
37
- "scripts": {
38
- "build": "vp pack",
39
- "dev": "vp pack --watch"
40
44
  }
41
- }
45
+ }
@@ -0,0 +1,102 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import {
6
+ getHaloPluginBundleLocation,
7
+ getHaloPluginManifest,
8
+ getHaloThemeAssetPublicPath,
9
+ getHaloThemeManifest,
10
+ getHaloThemeModuleName,
11
+ getManifestName,
12
+ } from "../utils/halo-plugin";
13
+
14
+ const tempDirs: string[] = [];
15
+
16
+ afterEach(() => {
17
+ vi.restoreAllMocks();
18
+ while (tempDirs.length > 0) {
19
+ const tempDir = tempDirs.pop();
20
+ if (tempDir) {
21
+ fs.rmSync(tempDir, { recursive: true, force: true });
22
+ }
23
+ }
24
+ });
25
+
26
+ describe("halo manifest utilities", () => {
27
+ it("reads plugin manifests", () => {
28
+ const manifestPath = writeManifest([
29
+ "metadata:",
30
+ " name: plugin-a",
31
+ "spec:",
32
+ " requires: '>=2.25.0'",
33
+ "",
34
+ ]);
35
+
36
+ const manifest = getHaloPluginManifest(manifestPath);
37
+
38
+ expect(getManifestName(manifest)).toBe("plugin-a");
39
+ expect(manifest.spec.requires).toBe(">=2.25.0");
40
+ });
41
+
42
+ it("reads theme manifests and derives theme bundle values", () => {
43
+ const manifestPath = writeManifest(["metadata:", " name: theme-a", ""]);
44
+
45
+ const manifest = getHaloThemeManifest(manifestPath);
46
+
47
+ expect(getManifestName(manifest)).toBe("theme-a");
48
+ expect(getHaloThemeModuleName(manifest)).toBe("theme:theme-a");
49
+ expect(getHaloThemeAssetPublicPath(manifest)).toBe(
50
+ "/themes/theme-a/ui-plugin/assets/"
51
+ );
52
+ });
53
+
54
+ it("selects ui bundle location for plugins requiring Halo 2.25 or newer", () => {
55
+ expect(
56
+ getHaloPluginBundleLocation({
57
+ metadata: { name: "plugin-a" },
58
+ spec: { requires: ">=2.25.0" },
59
+ } as never)
60
+ ).toBe("ui");
61
+ });
62
+
63
+ it("falls back to console bundle location for older or missing requirements", () => {
64
+ expect(
65
+ getHaloPluginBundleLocation({
66
+ metadata: { name: "plugin-a" },
67
+ spec: { requires: ">=2.24.0" },
68
+ } as never)
69
+ ).toBe("console");
70
+ expect(
71
+ getHaloPluginBundleLocation({
72
+ metadata: { name: "plugin-a" },
73
+ spec: {},
74
+ } as never)
75
+ ).toBe("console");
76
+ });
77
+
78
+ it("warns and falls back to console bundle location for invalid requirements", () => {
79
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
80
+
81
+ expect(
82
+ getHaloPluginBundleLocation({
83
+ metadata: { name: "plugin-a" },
84
+ spec: { requires: "not semver" },
85
+ } as never)
86
+ ).toBe("console");
87
+ expect(warn).toHaveBeenCalledWith(
88
+ '[ui-plugin-bundler-kit] Invalid semver range in plugin manifest "spec.requires": "not semver". ' +
89
+ 'Falling back to "console" bundle location.'
90
+ );
91
+ });
92
+ });
93
+
94
+ function writeManifest(lines: string[]) {
95
+ const tempDir = fs.mkdtempSync(
96
+ path.join(os.tmpdir(), "halo-ui-plugin-manifest-")
97
+ );
98
+ tempDirs.push(tempDir);
99
+ const manifestPath = path.join(tempDir, "manifest.yaml");
100
+ fs.writeFileSync(manifestPath, lines.join("\n"));
101
+ return manifestPath;
102
+ }
@@ -0,0 +1,128 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import type { Plugin } from "vite";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import { HaloUIPluginBundlerKit } from "../legacy";
7
+
8
+ const originalCwd = process.cwd();
9
+ const tempDirs: string[] = [];
10
+
11
+ afterEach(() => {
12
+ process.chdir(originalCwd);
13
+ while (tempDirs.length > 0) {
14
+ const tempDir = tempDirs.pop();
15
+ if (tempDir) {
16
+ fs.rmSync(tempDir, { recursive: true, force: true });
17
+ }
18
+ }
19
+ });
20
+
21
+ describe("HaloUIPluginBundlerKit", () => {
22
+ it("keeps legacy default output directories", () => {
23
+ const uiDir = setupPluginProject("legacy-plugin");
24
+ process.chdir(uiDir);
25
+
26
+ const plugin = HaloUIPluginBundlerKit();
27
+
28
+ expect(resolveLegacyConfig(plugin, "development").build).toMatchObject({
29
+ outDir: "../build/resources/main/console",
30
+ emptyOutDir: true,
31
+ lib: {
32
+ entry: "src/index.ts",
33
+ name: "legacy-plugin",
34
+ formats: ["iife"],
35
+ },
36
+ });
37
+ expect(resolveLegacyConfig(plugin, "production").build).toMatchObject({
38
+ outDir: "../src/main/resources/console",
39
+ });
40
+ });
41
+
42
+ it("applies string outDir override for every mode", () => {
43
+ const uiDir = setupPluginProject("legacy-plugin");
44
+ process.chdir(uiDir);
45
+
46
+ const plugin = HaloUIPluginBundlerKit({ outDir: "custom" });
47
+
48
+ expect(resolveLegacyConfig(plugin, "development").build).toMatchObject({
49
+ outDir: "custom",
50
+ });
51
+ expect(resolveLegacyConfig(plugin, "production").build).toMatchObject({
52
+ outDir: "custom",
53
+ });
54
+ });
55
+
56
+ it("applies mode-specific outDir overrides", () => {
57
+ const uiDir = setupPluginProject("legacy-plugin");
58
+ process.chdir(uiDir);
59
+
60
+ const plugin = HaloUIPluginBundlerKit({
61
+ outDir: {
62
+ dev: "dev-dist",
63
+ prod: "prod-dist",
64
+ },
65
+ });
66
+
67
+ expect(resolveLegacyConfig(plugin, "development").build).toMatchObject({
68
+ outDir: "dev-dist",
69
+ });
70
+ expect(resolveLegacyConfig(plugin, "production").build).toMatchObject({
71
+ outDir: "prod-dist",
72
+ });
73
+ });
74
+
75
+ it("uses custom manifest path", () => {
76
+ const projectRoot = createTempDir();
77
+ const manifestPath = path.join(projectRoot, "plugin.yaml");
78
+ fs.writeFileSync(
79
+ manifestPath,
80
+ ["metadata:", " name: custom-legacy-plugin", "spec:", ""].join("\n")
81
+ );
82
+
83
+ const plugin = HaloUIPluginBundlerKit({ manifestPath });
84
+
85
+ expect(resolveLegacyConfig(plugin, "production").build).toMatchObject({
86
+ lib: {
87
+ name: "custom-legacy-plugin",
88
+ },
89
+ });
90
+ });
91
+ });
92
+
93
+ function resolveLegacyConfig(plugin: Plugin, mode: string) {
94
+ if (typeof plugin.config !== "function") {
95
+ throw new Error("Expected plugin config hook");
96
+ }
97
+ return plugin.config(
98
+ {},
99
+ {
100
+ command: "build",
101
+ mode,
102
+ isSsrBuild: false,
103
+ isPreview: false,
104
+ }
105
+ );
106
+ }
107
+
108
+ function setupPluginProject(name: string) {
109
+ const projectRoot = createTempDir();
110
+ const uiDir = path.join(projectRoot, "ui");
111
+ fs.mkdirSync(path.join(projectRoot, "src/main/resources"), {
112
+ recursive: true,
113
+ });
114
+ fs.mkdirSync(uiDir, { recursive: true });
115
+ fs.writeFileSync(
116
+ path.join(projectRoot, "src/main/resources/plugin.yaml"),
117
+ ["metadata:", ` name: ${name}`, "spec:", ""].join("\n")
118
+ );
119
+ return uiDir;
120
+ }
121
+
122
+ function createTempDir() {
123
+ const tempDir = fs.mkdtempSync(
124
+ path.join(os.tmpdir(), "halo-ui-plugin-legacy-")
125
+ );
126
+ tempDirs.push(tempDir);
127
+ return tempDir;
128
+ }
@@ -0,0 +1,301 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import type { ConfigParams, RsbuildConfig } from "@rsbuild/core";
5
+ import type { ConfigEnv, UserConfig } from "vite";
6
+ import { afterEach, describe, expect, it } from "vitest";
7
+ import { rsbuildConfig } from "../rsbuild";
8
+ import { viteConfig } from "../vite";
9
+
10
+ const originalCwd = process.cwd();
11
+ const tempDirs: string[] = [];
12
+
13
+ afterEach(() => {
14
+ process.chdir(originalCwd);
15
+ while (tempDirs.length > 0) {
16
+ const tempDir = tempDirs.pop();
17
+ if (tempDir) {
18
+ fs.rmSync(tempDir, { recursive: true, force: true });
19
+ }
20
+ }
21
+ });
22
+
23
+ describe("provider defaults", () => {
24
+ it("keeps plugin provider defaults when provider is omitted", () => {
25
+ const uiDir = setupPluginProject();
26
+ process.chdir(uiDir);
27
+
28
+ const vite = resolveViteConfig(viteConfig({ vite: {} }), "development");
29
+ expect(vite.base).toBeUndefined();
30
+ expect(vite.build?.outDir).toBe("../build/resources/main/ui");
31
+ expect(vite.build?.lib).toMatchObject({
32
+ entry: "src/index.ts",
33
+ name: "fake-plugin",
34
+ formats: ["iife"],
35
+ cssFileName: "style",
36
+ });
37
+
38
+ const rsbuild = resolveRsbuildConfig(
39
+ rsbuildConfig({ rsbuild: {} }),
40
+ "development"
41
+ );
42
+ expect(rsbuild.output?.distPath?.root).toBe("../build/resources/main/ui");
43
+ expect(rsbuild.tools?.rspack?.output?.publicPath).toBe(
44
+ "/plugins/fake-plugin/assets/ui/"
45
+ );
46
+ expect(rsbuild.tools?.rspack?.output?.library).toMatchObject({
47
+ type: "window",
48
+ export: "default",
49
+ name: "fake-plugin",
50
+ });
51
+
52
+ const productionVite = resolveViteConfig(viteConfig({ vite: {} }));
53
+ expect(productionVite.build?.outDir).toBe("./build/dist");
54
+
55
+ const productionRsbuild = resolveRsbuildConfig(
56
+ rsbuildConfig({ rsbuild: {} })
57
+ );
58
+ expect(productionRsbuild.output?.distPath?.root).toBe("./build/dist");
59
+ });
60
+
61
+ it("keeps plugin provider defaults when provider is explicit", () => {
62
+ const uiDir = setupPluginProject();
63
+ process.chdir(uiDir);
64
+
65
+ const vite = resolveViteConfig(
66
+ viteConfig({ provider: "plugin", vite: {} }),
67
+ "development"
68
+ );
69
+ expect(vite.build?.outDir).toBe("../build/resources/main/ui");
70
+
71
+ const rsbuild = resolveRsbuildConfig(
72
+ rsbuildConfig({ provider: "plugin", rsbuild: {} }),
73
+ "development"
74
+ );
75
+ expect(rsbuild.tools?.rspack?.output?.publicPath).toBe(
76
+ "/plugins/fake-plugin/assets/ui/"
77
+ );
78
+ });
79
+
80
+ it("uses custom manifest paths for plugin and theme providers", () => {
81
+ const projectRoot = createTempDir();
82
+ const pluginManifestPath = path.join(projectRoot, "custom-plugin.yaml");
83
+ const themeManifestPath = path.join(projectRoot, "custom-theme.yaml");
84
+ fs.writeFileSync(
85
+ pluginManifestPath,
86
+ [
87
+ "metadata:",
88
+ " name: custom-plugin",
89
+ "spec:",
90
+ " requires: '>=2.25.0'",
91
+ "",
92
+ ].join("\n")
93
+ );
94
+ fs.writeFileSync(
95
+ themeManifestPath,
96
+ ["metadata:", " name: custom-theme", ""].join("\n")
97
+ );
98
+
99
+ const pluginConfig = resolveViteConfig(
100
+ viteConfig({
101
+ manifestPath: pluginManifestPath,
102
+ vite: {},
103
+ })
104
+ );
105
+ expect(pluginConfig.build?.lib).toMatchObject({
106
+ name: "custom-plugin",
107
+ });
108
+
109
+ const themeConfig = resolveRsbuildConfig(
110
+ rsbuildConfig({
111
+ provider: "theme",
112
+ manifestPath: themeManifestPath,
113
+ rsbuild: {},
114
+ })
115
+ );
116
+ expect(themeConfig.tools?.rspack?.output?.publicPath).toBe(
117
+ "/themes/custom-theme/ui-plugin/assets/"
118
+ );
119
+ expect(themeConfig.tools?.rspack?.output?.library).toMatchObject({
120
+ name: "theme:custom-theme",
121
+ });
122
+ });
123
+
124
+ it("generates Vite theme provider defaults", () => {
125
+ const uiPluginDir = setupThemeProject();
126
+ process.chdir(uiPluginDir);
127
+
128
+ const config = resolveViteConfig(
129
+ viteConfig({ provider: "theme", vite: {} })
130
+ );
131
+
132
+ expect(config.base).toBe("/themes/earth/ui-plugin/assets/");
133
+ expect(config.build?.outDir).toBe("dist");
134
+ expect(config.build?.lib).toMatchObject({
135
+ entry: "src/index.ts",
136
+ name: "theme:earth",
137
+ formats: ["iife"],
138
+ cssFileName: "style",
139
+ });
140
+ expect(config.build?.rollupOptions?.external).toContain("vue");
141
+ expect(config.build?.rollupOptions?.output).toMatchObject({
142
+ globals: expect.objectContaining({
143
+ vue: "Vue",
144
+ }),
145
+ extend: true,
146
+ });
147
+ });
148
+
149
+ it("generates Rsbuild theme provider defaults", () => {
150
+ const uiPluginDir = setupThemeProject();
151
+ process.chdir(uiPluginDir);
152
+
153
+ const config = resolveRsbuildConfig(
154
+ rsbuildConfig({ provider: "theme", rsbuild: {} })
155
+ );
156
+
157
+ expect(config.output?.distPath?.root).toBe("dist");
158
+ expect(config.output?.filename?.js).toBeDefined();
159
+ expect(config.output?.filename?.css).toBeDefined();
160
+ expect(config.output?.externals).toMatchObject({
161
+ vue: "Vue",
162
+ });
163
+ expect(config.tools?.rspack?.output?.publicPath).toBe(
164
+ "/themes/earth/ui-plugin/assets/"
165
+ );
166
+ expect(config.tools?.rspack?.output?.library).toMatchObject({
167
+ type: "window",
168
+ export: "default",
169
+ name: "theme:earth",
170
+ });
171
+ });
172
+
173
+ it("merges user config after provider defaults", () => {
174
+ const uiPluginDir = setupThemeProject();
175
+ process.chdir(uiPluginDir);
176
+
177
+ const vite = resolveViteConfig(
178
+ viteConfig({
179
+ provider: "theme",
180
+ vite: {
181
+ base: "/custom/",
182
+ build: {
183
+ outDir: "custom-dist",
184
+ },
185
+ },
186
+ })
187
+ );
188
+ expect(vite.base).toBe("/custom/");
189
+ expect(vite.build?.outDir).toBe("custom-dist");
190
+
191
+ const rsbuild = resolveRsbuildConfig(
192
+ rsbuildConfig({
193
+ provider: "theme",
194
+ rsbuild: {
195
+ output: {
196
+ distPath: {
197
+ root: "custom-dist",
198
+ },
199
+ },
200
+ tools: {
201
+ rspack: {
202
+ output: {
203
+ publicPath: "/custom/",
204
+ },
205
+ },
206
+ },
207
+ },
208
+ })
209
+ );
210
+ expect(rsbuild.output?.distPath?.root).toBe("custom-dist");
211
+ expect(rsbuild.tools?.rspack?.output?.publicPath).toBe("/custom/");
212
+ });
213
+
214
+ it("merges function user config after provider defaults", () => {
215
+ const uiPluginDir = setupThemeProject();
216
+ process.chdir(uiPluginDir);
217
+
218
+ const vite = resolveViteConfig(
219
+ viteConfig({
220
+ provider: "theme",
221
+ vite: ({ mode }) => ({
222
+ define: {
223
+ __MODE__: JSON.stringify(mode),
224
+ },
225
+ }),
226
+ })
227
+ );
228
+ expect(vite.define).toMatchObject({
229
+ "process.env.NODE_ENV": "'production'",
230
+ __MODE__: '"production"',
231
+ });
232
+
233
+ const rsbuild = resolveRsbuildConfig(
234
+ rsbuildConfig({
235
+ provider: "theme",
236
+ rsbuild: ({ envMode }) => ({
237
+ output: {
238
+ filename: {
239
+ js: `custom-${envMode}.js`,
240
+ },
241
+ },
242
+ }),
243
+ })
244
+ );
245
+ expect(rsbuild.output?.filename?.js).toBe("custom-production.js");
246
+ });
247
+ });
248
+
249
+ function setupPluginProject() {
250
+ const projectRoot = createTempDir();
251
+ const uiDir = path.join(projectRoot, "ui");
252
+ fs.mkdirSync(path.join(projectRoot, "src/main/resources"), {
253
+ recursive: true,
254
+ });
255
+ fs.mkdirSync(uiDir, { recursive: true });
256
+ fs.writeFileSync(
257
+ path.join(projectRoot, "src/main/resources/plugin.yaml"),
258
+ [
259
+ "metadata:",
260
+ " name: fake-plugin",
261
+ "spec:",
262
+ " requires: '>=2.25.0'",
263
+ "",
264
+ ].join("\n")
265
+ );
266
+ return uiDir;
267
+ }
268
+
269
+ function setupThemeProject() {
270
+ const projectRoot = createTempDir();
271
+ const uiPluginDir = path.join(projectRoot, "ui-plugin");
272
+ fs.mkdirSync(uiPluginDir, { recursive: true });
273
+ fs.writeFileSync(
274
+ path.join(projectRoot, "theme.yaml"),
275
+ ["metadata:", " name: earth", ""].join("\n")
276
+ );
277
+ return uiPluginDir;
278
+ }
279
+
280
+ function createTempDir() {
281
+ const tempDir = fs.mkdtempSync(
282
+ path.join(os.tmpdir(), "halo-ui-plugin-bundler-kit-")
283
+ );
284
+ tempDirs.push(tempDir);
285
+ return tempDir;
286
+ }
287
+
288
+ function resolveViteConfig(config: unknown, mode = "production") {
289
+ return (config as (env: ConfigEnv) => UserConfig)({
290
+ command: "build",
291
+ mode,
292
+ isSsrBuild: false,
293
+ isPreview: false,
294
+ });
295
+ }
296
+
297
+ function resolveRsbuildConfig(config: unknown, envMode = "production") {
298
+ return (config as (env: ConfigParams) => RsbuildConfig)({
299
+ envMode,
300
+ } as ConfigParams);
301
+ }
@@ -1,4 +1,15 @@
1
1
  const DEFAULT_OUT_DIR_DEV = "../build/resources/main/console";
2
+ const DEFAULT_OUT_DIR_DEV_BASE = "../build/resources/main";
2
3
  const DEFAULT_OUT_DIR_PROD = "./build/dist";
4
+ const DEFAULT_THEME_OUT_DIR = "dist";
3
5
 
4
- export { DEFAULT_OUT_DIR_DEV, DEFAULT_OUT_DIR_PROD };
6
+ function getDefaultOutDirDev(bundleLocation: string) {
7
+ return `${DEFAULT_OUT_DIR_DEV_BASE}/${bundleLocation}`;
8
+ }
9
+
10
+ export {
11
+ DEFAULT_OUT_DIR_DEV,
12
+ DEFAULT_OUT_DIR_PROD,
13
+ DEFAULT_THEME_OUT_DIR,
14
+ getDefaultOutDirDev,
15
+ };
@@ -9,6 +9,7 @@ const GLOBALS = {
9
9
  "@halo-dev/components": "HaloComponents",
10
10
  "@halo-dev/api-client": "HaloApiClient",
11
11
  "@halo-dev/richtext-editor": "RichTextEditor",
12
+ "@formkit/vue": "FormKitVue",
12
13
  axios: "axios",
13
14
  };
14
15
 
@@ -1,3 +1,9 @@
1
- const DEFAULT_MANIFEST_PATH = "../src/main/resources/plugin.yaml";
1
+ const DEFAULT_PLUGIN_MANIFEST_PATH = "../src/main/resources/plugin.yaml";
2
+ const DEFAULT_THEME_MANIFEST_PATH = "../theme.yaml";
3
+ const DEFAULT_MANIFEST_PATH = DEFAULT_PLUGIN_MANIFEST_PATH;
2
4
 
3
- export { DEFAULT_MANIFEST_PATH };
5
+ export {
6
+ DEFAULT_MANIFEST_PATH,
7
+ DEFAULT_PLUGIN_MANIFEST_PATH,
8
+ DEFAULT_THEME_MANIFEST_PATH,
9
+ };
package/src/rsbuild.ts CHANGED
@@ -6,16 +6,39 @@ import {
6
6
  type RsbuildMode,
7
7
  } from "@rsbuild/core";
8
8
  import { pluginVue } from "@rsbuild/plugin-vue";
9
- import { DEFAULT_OUT_DIR_DEV, DEFAULT_OUT_DIR_PROD } from "./constants/build";
9
+ import {
10
+ DEFAULT_OUT_DIR_PROD,
11
+ DEFAULT_THEME_OUT_DIR,
12
+ getDefaultOutDirDev,
13
+ } from "./constants/build";
10
14
  import { GLOBALS } from "./constants/externals";
11
- import { DEFAULT_MANIFEST_PATH } from "./constants/halo-plugin";
12
- import { getHaloPluginManifest } from "./utils/halo-plugin";
15
+ import {
16
+ DEFAULT_PLUGIN_MANIFEST_PATH,
17
+ DEFAULT_THEME_MANIFEST_PATH,
18
+ } from "./constants/halo-plugin";
19
+ import {
20
+ getHaloPluginBundleLocation,
21
+ getHaloPluginManifest,
22
+ getHaloThemeAssetPublicPath,
23
+ getHaloThemeManifest,
24
+ getHaloThemeModuleName,
25
+ getManifestName,
26
+ } from "./utils/halo-plugin";
27
+
28
+ type Provider = "plugin" | "theme";
13
29
 
14
30
  export interface RsBuildUserConfig {
15
31
  /**
16
- * Halo plugin manifest path.
32
+ * UI plugin provider type.
33
+ *
34
+ * @default "plugin"
35
+ */
36
+ provider?: "plugin" | "theme";
37
+
38
+ /**
39
+ * Halo plugin or theme manifest path.
17
40
  *
18
- * @default "../src/main/resources/plugin.yaml"
41
+ * @default "../src/main/resources/plugin.yaml" for plugins, "../theme.yaml" for themes
19
42
  */
20
43
  manifestPath?: string;
21
44
 
@@ -25,13 +48,16 @@ export interface RsBuildUserConfig {
25
48
  rsbuild: RsbuildConfig | ((env: ConfigParams) => RsbuildConfig);
26
49
  }
27
50
 
28
- function createRsbuildPresetsConfig(manifestPath: string) {
29
- const manifest = getHaloPluginManifest(manifestPath);
51
+ function createRsbuildPresetsConfig(provider: Provider, manifestPath: string) {
52
+ const defaults =
53
+ provider === "theme"
54
+ ? getThemeProviderDefaults(manifestPath)
55
+ : getPluginProviderDefaults(manifestPath);
30
56
 
31
57
  return defineConfig(({ envMode }) => {
32
58
  const isProduction = envMode === "production";
33
59
 
34
- const outDir = isProduction ? DEFAULT_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV;
60
+ const outDir = isProduction ? defaults.outDir.prod : defaults.outDir.dev;
35
61
 
36
62
  return {
37
63
  mode: (envMode as RsbuildMode) || "production",
@@ -72,11 +98,11 @@ function createRsbuildPresetsConfig(manifestPath: string) {
72
98
  },
73
99
  },
74
100
  output: {
75
- publicPath: `/plugins/${manifest.metadata.name}/assets/console/`,
101
+ publicPath: defaults.publicPath,
76
102
  library: {
77
103
  type: "window",
78
104
  export: "default",
79
- name: manifest.metadata.name,
105
+ name: defaults.moduleName,
80
106
  },
81
107
  globalObject: "window",
82
108
  iife: true,
@@ -113,6 +139,46 @@ function createRsbuildPresetsConfig(manifestPath: string) {
113
139
  });
114
140
  }
115
141
 
142
+ function getPluginProviderDefaults(manifestPath: string) {
143
+ const manifest = getHaloPluginManifest(manifestPath);
144
+ const bundleLocation = getHaloPluginBundleLocation(manifest);
145
+
146
+ return {
147
+ moduleName: getManifestName(manifest),
148
+ outDir: {
149
+ prod: DEFAULT_OUT_DIR_PROD,
150
+ dev: getDefaultOutDirDev(bundleLocation),
151
+ },
152
+ publicPath: `/plugins/${getManifestName(manifest)}/assets/${bundleLocation}/`,
153
+ };
154
+ }
155
+
156
+ function getThemeProviderDefaults(manifestPath: string) {
157
+ const manifest = getHaloThemeManifest(manifestPath);
158
+
159
+ return {
160
+ moduleName: getHaloThemeModuleName(manifest),
161
+ outDir: {
162
+ prod: DEFAULT_THEME_OUT_DIR,
163
+ dev: DEFAULT_THEME_OUT_DIR,
164
+ },
165
+ publicPath: getHaloThemeAssetPublicPath(manifest),
166
+ };
167
+ }
168
+
169
+ function getProvider(config?: RsBuildUserConfig): Provider {
170
+ return config?.provider || "plugin";
171
+ }
172
+
173
+ function getManifestPath(provider: Provider, config?: RsBuildUserConfig) {
174
+ if (config?.manifestPath) {
175
+ return config.manifestPath;
176
+ }
177
+ return provider === "theme"
178
+ ? DEFAULT_THEME_MANIFEST_PATH
179
+ : DEFAULT_PLUGIN_MANIFEST_PATH;
180
+ }
181
+
116
182
  /**
117
183
  * Rsbuild config for Halo UI Plugin.
118
184
  *
@@ -132,8 +198,10 @@ function createRsbuildPresetsConfig(manifestPath: string) {
132
198
  export function rsbuildConfig(
133
199
  config?: RsBuildUserConfig
134
200
  ): (env: ConfigParams) => RsbuildConfig {
201
+ const provider = getProvider(config);
135
202
  const presetsConfigFn = createRsbuildPresetsConfig(
136
- config?.manifestPath || DEFAULT_MANIFEST_PATH
203
+ provider,
204
+ getManifestPath(provider, config)
137
205
  );
138
206
  return defineConfig((env) => {
139
207
  const presetsConfig = presetsConfigFn(env);
@@ -1,11 +1,67 @@
1
1
  import fs from "node:fs";
2
2
  import type { Plugin as HaloPlugin } from "@halo-dev/api-client";
3
3
  import yaml from "js-yaml";
4
+ import { gte, minVersion } from "semver";
5
+
6
+ const UI_BUNDLE_MIN_HALO_VERSION = "2.25.0";
7
+ const UI_BUNDLE_LOCATION = "ui";
8
+ const CONSOLE_BUNDLE_LOCATION = "console";
9
+ const THEME_MODULE_NAME_PREFIX = "theme:";
10
+
11
+ interface HaloThemeManifest {
12
+ metadata: {
13
+ name: string;
14
+ };
15
+ }
4
16
 
5
17
  export function getHaloPluginManifest(manifestPath: string) {
6
- const manifest = yaml.load(
7
- fs.readFileSync(manifestPath, "utf8")
8
- ) as HaloPlugin;
18
+ return readManifest<HaloPlugin>(manifestPath);
19
+ }
20
+
21
+ export function getHaloThemeManifest(manifestPath: string) {
22
+ return readManifest<HaloThemeManifest>(manifestPath);
23
+ }
24
+
25
+ export function getManifestName(
26
+ manifest: Pick<HaloPlugin, "metadata"> | HaloThemeManifest
27
+ ) {
28
+ return manifest.metadata.name;
29
+ }
30
+
31
+ export function getHaloThemeModuleName(manifest: HaloThemeManifest) {
32
+ return `${THEME_MODULE_NAME_PREFIX}${getManifestName(manifest)}`;
33
+ }
34
+
35
+ export function getHaloThemeAssetPublicPath(manifest: HaloThemeManifest) {
36
+ return `/themes/${getManifestName(manifest)}/ui-plugin/assets/`;
37
+ }
38
+
39
+ export function getHaloPluginBundleLocation(manifest: HaloPlugin) {
40
+ const requiresMinVersion = getRequiresMinVersion(manifest.spec.requires);
41
+ return requiresMinVersion &&
42
+ gte(requiresMinVersion, UI_BUNDLE_MIN_HALO_VERSION)
43
+ ? UI_BUNDLE_LOCATION
44
+ : CONSOLE_BUNDLE_LOCATION;
45
+ }
46
+
47
+ function getRequiresMinVersion(requires: string | undefined) {
48
+ const normalizedRequires = requires?.trim();
49
+
50
+ if (!normalizedRequires) {
51
+ return;
52
+ }
53
+
54
+ try {
55
+ return minVersion(normalizedRequires);
56
+ } catch {
57
+ console.warn(
58
+ `[ui-plugin-bundler-kit] Invalid semver range in plugin manifest "spec.requires": "${requires}". ` +
59
+ `Falling back to "${CONSOLE_BUNDLE_LOCATION}" bundle location.`
60
+ );
61
+ return;
62
+ }
63
+ }
9
64
 
10
- return manifest;
65
+ function readManifest<T>(manifestPath: string) {
66
+ return yaml.load(fs.readFileSync(manifestPath, "utf8")) as T;
11
67
  }
package/src/vite.ts CHANGED
@@ -5,16 +5,39 @@ import {
5
5
  UserConfig,
6
6
  UserConfigFnObject,
7
7
  } from "vite";
8
- import { DEFAULT_OUT_DIR_DEV, DEFAULT_OUT_DIR_PROD } from "./constants/build";
8
+ import {
9
+ DEFAULT_OUT_DIR_PROD,
10
+ DEFAULT_THEME_OUT_DIR,
11
+ getDefaultOutDirDev,
12
+ } from "./constants/build";
9
13
  import { EXTERNALS, GLOBALS } from "./constants/externals";
10
- import { DEFAULT_MANIFEST_PATH } from "./constants/halo-plugin";
11
- import { getHaloPluginManifest } from "./utils/halo-plugin";
14
+ import {
15
+ DEFAULT_PLUGIN_MANIFEST_PATH,
16
+ DEFAULT_THEME_MANIFEST_PATH,
17
+ } from "./constants/halo-plugin";
18
+ import {
19
+ getHaloPluginBundleLocation,
20
+ getHaloPluginManifest,
21
+ getHaloThemeAssetPublicPath,
22
+ getHaloThemeManifest,
23
+ getHaloThemeModuleName,
24
+ getManifestName,
25
+ } from "./utils/halo-plugin";
26
+
27
+ type Provider = "plugin" | "theme";
12
28
 
13
29
  export interface ViteUserConfig {
14
30
  /**
15
- * Halo plugin manifest path.
31
+ * UI plugin provider type.
32
+ *
33
+ * @default "plugin"
34
+ */
35
+ provider?: "plugin" | "theme";
36
+
37
+ /**
38
+ * Halo plugin or theme manifest path.
16
39
  *
17
- * @default "../src/main/resources/plugin.yaml"
40
+ * @default "../src/main/resources/plugin.yaml" for plugins, "../theme.yaml" for themes
18
41
  */
19
42
  manifestPath?: string;
20
43
 
@@ -24,22 +47,26 @@ export interface ViteUserConfig {
24
47
  vite: UserConfig | UserConfigFnObject;
25
48
  }
26
49
 
27
- function createVitePresetsConfig(manifestPath: string) {
28
- const manifest = getHaloPluginManifest(manifestPath);
50
+ function createVitePresetsConfig(provider: Provider, manifestPath: string) {
51
+ const defaults =
52
+ provider === "theme"
53
+ ? getThemeProviderDefaults(manifestPath)
54
+ : getPluginProviderDefaults(manifestPath);
29
55
 
30
56
  return defineConfig(({ mode }) => {
31
57
  const isProduction = mode === "production";
32
58
 
33
59
  return {
34
60
  mode: mode || "production",
61
+ base: defaults.base,
35
62
  plugins: [Vue()],
36
63
  define: { "process.env.NODE_ENV": "'production'" },
37
64
  build: {
38
- outDir: isProduction ? DEFAULT_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV,
65
+ outDir: isProduction ? defaults.outDir.prod : defaults.outDir.dev,
39
66
  emptyOutDir: true,
40
67
  lib: {
41
68
  entry: "src/index.ts",
42
- name: manifest.metadata.name,
69
+ name: defaults.moduleName,
43
70
  formats: ["iife"],
44
71
  fileName: () => "main.js",
45
72
  cssFileName: "style",
@@ -56,6 +83,46 @@ function createVitePresetsConfig(manifestPath: string) {
56
83
  });
57
84
  }
58
85
 
86
+ function getPluginProviderDefaults(manifestPath: string) {
87
+ const manifest = getHaloPluginManifest(manifestPath);
88
+ const bundleLocation = getHaloPluginBundleLocation(manifest);
89
+
90
+ return {
91
+ moduleName: getManifestName(manifest),
92
+ outDir: {
93
+ prod: DEFAULT_OUT_DIR_PROD,
94
+ dev: getDefaultOutDirDev(bundleLocation),
95
+ },
96
+ base: undefined,
97
+ };
98
+ }
99
+
100
+ function getThemeProviderDefaults(manifestPath: string) {
101
+ const manifest = getHaloThemeManifest(manifestPath);
102
+
103
+ return {
104
+ moduleName: getHaloThemeModuleName(manifest),
105
+ outDir: {
106
+ prod: DEFAULT_THEME_OUT_DIR,
107
+ dev: DEFAULT_THEME_OUT_DIR,
108
+ },
109
+ base: getHaloThemeAssetPublicPath(manifest),
110
+ };
111
+ }
112
+
113
+ function getProvider(config?: ViteUserConfig): Provider {
114
+ return config?.provider || "plugin";
115
+ }
116
+
117
+ function getManifestPath(provider: Provider, config?: ViteUserConfig) {
118
+ if (config?.manifestPath) {
119
+ return config.manifestPath;
120
+ }
121
+ return provider === "theme"
122
+ ? DEFAULT_THEME_MANIFEST_PATH
123
+ : DEFAULT_PLUGIN_MANIFEST_PATH;
124
+ }
125
+
59
126
  /**
60
127
  * Vite config for Halo UI Plugin.
61
128
  *
@@ -71,8 +138,10 @@ function createVitePresetsConfig(manifestPath: string) {
71
138
  * ```
72
139
  */
73
140
  export function viteConfig(config?: ViteUserConfig) {
141
+ const provider = getProvider(config);
74
142
  const presetsConfigFn = createVitePresetsConfig(
75
- config?.manifestPath || DEFAULT_MANIFEST_PATH
143
+ provider,
144
+ getManifestPath(provider, config)
76
145
  );
77
146
  return defineConfig((env) => {
78
147
  const presetsConfig = presetsConfigFn(env);