@rainbow-robotics/plugin-ui 1.10.1-dev.1 → 1.10.2-dev.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/src/viteHost.ts CHANGED
@@ -1,309 +1,309 @@
1
- /**
2
- * Vite plugins for a HOST app previewing locally-developed plugins.
3
- * Host-agnostic — paths are parameters.
4
- *
5
- * import {
6
- * pluginPreviewRuntimePlugin,
7
- * pluginPreviewAssetsPlugin,
8
- * isPluginPreviewMode,
9
- * } from "@rainbow-robotics/plugin-ui/vite-host";
10
- *
11
- * A built plugin's bare imports (react, rb-components, plugin-ui/*) resolve via
12
- * the import map this file injects, to the host's already-loaded instances.
13
- * See ./host.tsx.
14
- */
15
- import type { Plugin } from "vite";
16
- import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
17
- import { createRequire } from "node:module";
18
- import { extname, resolve } from "node:path";
19
-
20
- const require = createRequire(import.meta.url);
21
-
22
- export const defaultPreviewAssetPrefixes = [
23
- "/imgs/",
24
- "/fonts/",
25
- "/models/",
26
- "/sounds/",
27
- "/data/",
28
- ];
29
-
30
- export const contentTypeByExt: Record<string, string> = {
31
- ".avif": "image/avif",
32
- ".css": "text/css; charset=utf-8",
33
- ".gif": "image/gif",
34
- ".glb": "model/gltf-binary",
35
- ".jpeg": "image/jpeg",
36
- ".jpg": "image/jpeg",
37
- ".js": "text/javascript; charset=utf-8",
38
- ".json": "application/json; charset=utf-8",
39
- ".mp3": "audio/mpeg",
40
- ".png": "image/png",
41
- ".svg": "image/svg+xml",
42
- ".wasm": "application/wasm",
43
- ".webp": "image/webp",
44
- ".woff": "font/woff",
45
- ".woff2": "font/woff2",
46
- };
47
-
48
- export const isPluginPreviewMode = (mode: string) => mode === "plugin-preview";
49
-
50
- /* -------------------------------------------------------------------------- */
51
- /* Shared-runtime import map (plugins reuse the host's loaded instances) */
52
- /* -------------------------------------------------------------------------- */
53
-
54
- const SHARED_BASE = "/plugin-shared";
55
-
56
- // Each shared specifier → the named exports its shim re-exports from
57
- // window.__RB_SHARED__[specifier] (set by the host, e.g. nexus/src/pluginShared).
58
- //
59
- // `names`: hardcoded list (small, stable modules).
60
- // `parseNamesFrom`: parse the export names out of a source file's text — used
61
- // for react.tsx, which re-exports the whole rb-components barrel (100+ names).
62
- // We text-parse rather than import it: the barrel is .ts source, which plain
63
- // Node can't execute.
64
- const SHARED_MODULES: Record<
65
- string,
66
- {
67
- file: string;
68
- names?: string[];
69
- parseNamesFrom?: { specifier: string; extraNames?: string[] };
70
- hasDefault?: boolean;
71
- }
72
- > = {
73
- react: {
74
- file: "react",
75
- hasDefault: true,
76
- names: [
77
- "Children", "Component", "Fragment", "Profiler", "PureComponent",
78
- "StrictMode", "Suspense", "cloneElement", "createContext",
79
- "createElement", "createRef", "forwardRef", "isValidElement", "lazy",
80
- "memo", "startTransition", "useCallback", "useContext", "useDebugValue",
81
- "useDeferredValue", "useEffect", "useId", "useImperativeHandle",
82
- "useInsertionEffect", "useLayoutEffect", "useMemo", "useReducer",
83
- "useRef", "useState", "useSyncExternalStore", "useTransition", "version",
84
- ],
85
- },
86
- "react/jsx-runtime": {
87
- file: "react_jsx-runtime",
88
- names: ["Fragment", "jsx", "jsxs"],
89
- },
90
- "react/jsx-dev-runtime": {
91
- file: "react_jsx-dev-runtime",
92
- names: ["Fragment", "jsxDEV"],
93
- },
94
- "react-dom": {
95
- file: "react-dom",
96
- hasDefault: true,
97
- names: [
98
- "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED",
99
- "createPortal",
100
- "createRoot",
101
- "findDOMNode",
102
- "flushSync",
103
- "hydrate",
104
- "hydrateRoot",
105
- "render",
106
- "unmountComponentAtNode",
107
- "unstable_batchedUpdates",
108
- "unstable_renderSubtreeIntoContainer",
109
- "version",
110
- ],
111
- },
112
- "react-dom/client": {
113
- file: "react-dom_client",
114
- names: ["createRoot", "hydrateRoot"],
115
- },
116
- "@rainbow-robotics/plugin-ui/react": {
117
- file: "plugin-ui_react",
118
- // react.tsx re-exports the full rb-components barrel (export *) plus the sdk
119
- // hooks.
120
- parseNamesFrom: {
121
- specifier: "@repo/rb-components/new_components",
122
- extraNames: ["useRb", "useRbFormData", "useRbSave"],
123
- },
124
- },
125
- "@rainbow-robotics/plugin-ui/sdk": {
126
- file: "plugin-ui_sdk",
127
- names: ["RbContext", "useRb", "useRbFormData", "useRbSave"],
128
- },
129
- "@rainbow-robotics/plugin-ui/host-ui": {
130
- file: "plugin-ui_host-ui",
131
- names: [
132
- "alert",
133
- "confirm",
134
- "getHostServices",
135
- "prompt",
136
- "setHostServices",
137
- "toast",
138
- ],
139
- },
140
- "@rainbow-robotics/plugin-ui/ws": {
141
- file: "plugin-ui_ws",
142
- names: ["RbWs", "createLazyRbSdk"],
143
- },
144
- };
145
-
146
- // Parses `export { a, b as c, ... }` value names out of a source file's text
147
- // (skips `export type { ... }` blocks and individual `type X` entries within
148
- // a mixed block — types don't exist as runtime module properties). Static
149
- // text parsing, not module execution, so it works for .ts source files with
150
- // no loader and has no ESM-resolution footguns.
151
- function parseExportedValueNames(source: string): string[] {
152
- const names = new Set<string>();
153
- const blockRe = /export\s+(type\s+)?\{([^}]*)\}/g;
154
- let match: RegExpExecArray | null;
155
- while ((match = blockRe.exec(source))) {
156
- if (match[1]) continue; // whole `export type { ... }` block
157
- for (const rawEntry of match[2].split(",")) {
158
- const entry = rawEntry.trim();
159
- if (!entry || entry.startsWith("type ")) continue;
160
- const asMatch = entry.match(/^\S+\s+as\s+(\S+)$/);
161
- names.add(asMatch ? asMatch[1] : entry);
162
- }
163
- }
164
- return [...names];
165
- }
166
-
167
- const nameCache = new Map<string, string[]>();
168
-
169
- function resolveNames(spec: string): string[] {
170
- const mod = SHARED_MODULES[spec];
171
- if (mod.names) return mod.names;
172
- if (!mod.parseNamesFrom) {
173
- throw new Error(`[plugin-ui] ${spec} has neither "names" nor "parseNamesFrom"`);
174
- }
175
- if (!nameCache.has(spec)) {
176
- const { specifier, extraNames = [] } = mod.parseNamesFrom;
177
- const sourcePath = require.resolve(specifier);
178
- const parsed = parseExportedValueNames(readFileSync(sourcePath, "utf8"));
179
- nameCache.set(spec, [...new Set([...parsed, ...extraNames])]);
180
- }
181
- return nameCache.get(spec)!;
182
- }
183
-
184
- function shimSource(spec: string): string {
185
- const mod = SHARED_MODULES[spec];
186
- const names = resolveNames(spec);
187
- const g = `(globalThis.__RB_SHARED__||{})[${JSON.stringify(spec)}]`;
188
- return (
189
- `const m = ${g};\n` +
190
- `if (!m) throw new Error("[plugin-ui] host shared module not registered: ${spec}");\n` +
191
- `export const { ${names.join(", ")} } = m;\n` +
192
- (mod.hasDefault ? `export default (m.default ?? m);\n` : "")
193
- );
194
- }
195
-
196
- const fileToSpec = Object.fromEntries(
197
- Object.entries(SHARED_MODULES).map(([spec, { file }]) => [file, spec]),
198
- );
199
-
200
- function importMapScript(): string {
201
- const imports: Record<string, string> = {};
202
- for (const [spec, { file }] of Object.entries(SHARED_MODULES)) {
203
- imports[spec] = `${SHARED_BASE}/${file}.js`;
204
- }
205
- return `<script type="importmap">${JSON.stringify({ imports })}</script>`;
206
- }
207
-
208
- /**
209
- * Serves the shared-runtime shim modules at /plugin-shared/*.js and injects the
210
- * import map so a dynamically-imported plugin's bare `import "react"` /
211
- * `import "@rainbow-robotics/plugin-ui/react"` etc. resolve to the host's OWN
212
- * instances (via window.__RB_SHARED__). Applied in all modes — any nexus build
213
- * that can host plugins needs it.
214
- */
215
- export const pluginSharedRuntimePlugin = (): Plugin => ({
216
- name: "plugin-ui-shared-runtime",
217
- transformIndexHtml(html) {
218
- return html.replace("</head>", ` ${importMapScript()}\n </head>`);
219
- },
220
- configureServer(server) {
221
- server.middlewares.use((req, res, next) => {
222
- const url = req.url?.split("?")[0];
223
- if (!url?.startsWith(`${SHARED_BASE}/`)) return next();
224
- const file = url.slice(`${SHARED_BASE}/`.length).replace(/\.js$/, "");
225
- const spec = fileToSpec[file];
226
- if (!spec) return next();
227
- res.setHeader("Content-Type", "text/javascript; charset=utf-8");
228
- res.setHeader("Access-Control-Allow-Origin", "*");
229
- res.end(shimSource(spec));
230
- });
231
- },
232
- generateBundle() {
233
- for (const [spec, { file }] of Object.entries(SHARED_MODULES)) {
234
- this.emitFile({
235
- type: "asset",
236
- fileName: `plugin-shared/${file}.js`,
237
- source: shimSource(spec),
238
- });
239
- }
240
- },
241
- });
242
-
243
- /** Inject the preview-host runtime flags into index.html (plugin-preview only). */
244
- export const pluginPreviewRuntimePlugin = (): Plugin => ({
245
- name: "plugin-ui-preview-runtime",
246
- apply: (_config, { mode }) => isPluginPreviewMode(mode),
247
- transformIndexHtml(html) {
248
- return html.replace(
249
- "</head>",
250
- [
251
- " <script>",
252
- " window.__RB_PLUGIN_PREVIEW_HOST__ = true;",
253
- " window.__RB_ASSETS_BASE_URL__ = window.location.origin;",
254
- " </script>",
255
- " </head>",
256
- ].join("\n"),
257
- );
258
- },
259
- });
260
-
261
- /** Serve the host's static assets (imgs/fonts/...) in plugin-preview dev mode. */
262
- export const pluginPreviewAssetsPlugin = (opts: {
263
- assetsDir: string;
264
- prefixes?: string[];
265
- }): Plugin => {
266
- const { assetsDir, prefixes = defaultPreviewAssetPrefixes } = opts;
267
- return {
268
- name: "plugin-ui-preview-assets",
269
- apply: (_config, { command, mode }) =>
270
- command === "serve" && isPluginPreviewMode(mode),
271
- configureServer(server) {
272
- server.middlewares.use((req, res, next) => {
273
- const url = req.url?.split("?")[0];
274
- if (!url || !prefixes.some((prefix) => url.startsWith(prefix)))
275
- return next();
276
-
277
- const decodedUrl = decodeURIComponent(url);
278
- const filePath = resolve(assetsDir, `.${decodedUrl}`);
279
- if (!filePath.startsWith(assetsDir)) return next();
280
- if (!existsSync(filePath) || !statSync(filePath).isFile()) return next();
281
-
282
- const contentType = contentTypeByExt[extname(filePath)];
283
- if (contentType) res.setHeader("Content-Type", contentType);
284
- res.setHeader("Access-Control-Allow-Origin", "*");
285
- createReadStream(filePath).pipe(res);
286
- });
287
- },
288
- };
289
- };
290
-
291
- /**
292
- * All host-side plugin-ui Vite plugins in one call. Each self-gates by mode, so
293
- * a host just spreads this into its `plugins` array.
294
- *
295
- * plugins: [muscatVitePlugin({ previewAssetsDir: resolve(here, "../assets/public") })]
296
- */
297
- export const muscatVitePlugin = (opts: {
298
- /** Host static assets dir served in plugin-preview dev mode. */
299
- previewAssetsDir: string;
300
- /** Override preview asset URL prefixes. */
301
- previewAssetPrefixes?: string[];
302
- }): Plugin[] => [
303
- pluginSharedRuntimePlugin(),
304
- pluginPreviewRuntimePlugin(),
305
- pluginPreviewAssetsPlugin({
306
- assetsDir: opts.previewAssetsDir,
307
- prefixes: opts.previewAssetPrefixes,
308
- }),
309
- ];
1
+ /**
2
+ * Vite plugins for a HOST app previewing locally-developed plugins.
3
+ * Host-agnostic — paths are parameters.
4
+ *
5
+ * import {
6
+ * pluginPreviewRuntimePlugin,
7
+ * pluginPreviewAssetsPlugin,
8
+ * isPluginPreviewMode,
9
+ * } from "@rainbow-robotics/plugin-ui/vite-host";
10
+ *
11
+ * A built plugin's bare imports (react, rb-components, plugin-ui/*) resolve via
12
+ * the import map this file injects, to the host's already-loaded instances.
13
+ * See ./host.tsx.
14
+ */
15
+ import type { Plugin } from "vite";
16
+ import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
17
+ import { createRequire } from "node:module";
18
+ import { extname, resolve } from "node:path";
19
+
20
+ const require = createRequire(import.meta.url);
21
+
22
+ export const defaultPreviewAssetPrefixes = [
23
+ "/imgs/",
24
+ "/fonts/",
25
+ "/models/",
26
+ "/sounds/",
27
+ "/data/",
28
+ ];
29
+
30
+ export const contentTypeByExt: Record<string, string> = {
31
+ ".avif": "image/avif",
32
+ ".css": "text/css; charset=utf-8",
33
+ ".gif": "image/gif",
34
+ ".glb": "model/gltf-binary",
35
+ ".jpeg": "image/jpeg",
36
+ ".jpg": "image/jpeg",
37
+ ".js": "text/javascript; charset=utf-8",
38
+ ".json": "application/json; charset=utf-8",
39
+ ".mp3": "audio/mpeg",
40
+ ".png": "image/png",
41
+ ".svg": "image/svg+xml",
42
+ ".wasm": "application/wasm",
43
+ ".webp": "image/webp",
44
+ ".woff": "font/woff",
45
+ ".woff2": "font/woff2",
46
+ };
47
+
48
+ export const isPluginPreviewMode = (mode: string) => mode === "plugin-preview";
49
+
50
+ /* -------------------------------------------------------------------------- */
51
+ /* Shared-runtime import map (plugins reuse the host's loaded instances) */
52
+ /* -------------------------------------------------------------------------- */
53
+
54
+ const SHARED_BASE = "/plugin-shared";
55
+
56
+ // Each shared specifier → the named exports its shim re-exports from
57
+ // window.__RB_SHARED__[specifier] (set by the host, e.g. nexus/src/pluginShared).
58
+ //
59
+ // `names`: hardcoded list (small, stable modules).
60
+ // `parseNamesFrom`: parse the export names out of a source file's text — used
61
+ // for react.tsx, which re-exports the whole rb-components barrel (100+ names).
62
+ // We text-parse rather than import it: the barrel is .ts source, which plain
63
+ // Node can't execute.
64
+ const SHARED_MODULES: Record<
65
+ string,
66
+ {
67
+ file: string;
68
+ names?: string[];
69
+ parseNamesFrom?: { specifier: string; extraNames?: string[] };
70
+ hasDefault?: boolean;
71
+ }
72
+ > = {
73
+ react: {
74
+ file: "react",
75
+ hasDefault: true,
76
+ names: [
77
+ "Children", "Component", "Fragment", "Profiler", "PureComponent",
78
+ "StrictMode", "Suspense", "cloneElement", "createContext",
79
+ "createElement", "createRef", "forwardRef", "isValidElement", "lazy",
80
+ "memo", "startTransition", "useCallback", "useContext", "useDebugValue",
81
+ "useDeferredValue", "useEffect", "useId", "useImperativeHandle",
82
+ "useInsertionEffect", "useLayoutEffect", "useMemo", "useReducer",
83
+ "useRef", "useState", "useSyncExternalStore", "useTransition", "version",
84
+ ],
85
+ },
86
+ "react/jsx-runtime": {
87
+ file: "react_jsx-runtime",
88
+ names: ["Fragment", "jsx", "jsxs"],
89
+ },
90
+ "react/jsx-dev-runtime": {
91
+ file: "react_jsx-dev-runtime",
92
+ names: ["Fragment", "jsxDEV"],
93
+ },
94
+ "react-dom": {
95
+ file: "react-dom",
96
+ hasDefault: true,
97
+ names: [
98
+ "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED",
99
+ "createPortal",
100
+ "createRoot",
101
+ "findDOMNode",
102
+ "flushSync",
103
+ "hydrate",
104
+ "hydrateRoot",
105
+ "render",
106
+ "unmountComponentAtNode",
107
+ "unstable_batchedUpdates",
108
+ "unstable_renderSubtreeIntoContainer",
109
+ "version",
110
+ ],
111
+ },
112
+ "react-dom/client": {
113
+ file: "react-dom_client",
114
+ names: ["createRoot", "hydrateRoot"],
115
+ },
116
+ "@rainbow-robotics/plugin-ui/react": {
117
+ file: "plugin-ui_react",
118
+ // react.tsx re-exports the full rb-components barrel (export *) plus the sdk
119
+ // hooks.
120
+ parseNamesFrom: {
121
+ specifier: "@repo/rb-components/new_components",
122
+ extraNames: ["useRb", "useRbFormData", "useRbSave"],
123
+ },
124
+ },
125
+ "@rainbow-robotics/plugin-ui/sdk": {
126
+ file: "plugin-ui_sdk",
127
+ names: ["RbContext", "useRb", "useRbFormData", "useRbSave"],
128
+ },
129
+ "@rainbow-robotics/plugin-ui/host-ui": {
130
+ file: "plugin-ui_host-ui",
131
+ names: [
132
+ "alert",
133
+ "confirm",
134
+ "getHostServices",
135
+ "prompt",
136
+ "setHostServices",
137
+ "toast",
138
+ ],
139
+ },
140
+ "@rainbow-robotics/plugin-ui/ws": {
141
+ file: "plugin-ui_ws",
142
+ names: ["RbWs", "createLazyRbSdk"],
143
+ },
144
+ };
145
+
146
+ // Parses `export { a, b as c, ... }` value names out of a source file's text
147
+ // (skips `export type { ... }` blocks and individual `type X` entries within
148
+ // a mixed block — types don't exist as runtime module properties). Static
149
+ // text parsing, not module execution, so it works for .ts source files with
150
+ // no loader and has no ESM-resolution footguns.
151
+ function parseExportedValueNames(source: string): string[] {
152
+ const names = new Set<string>();
153
+ const blockRe = /export\s+(type\s+)?\{([^}]*)\}/g;
154
+ let match: RegExpExecArray | null;
155
+ while ((match = blockRe.exec(source))) {
156
+ if (match[1]) continue; // whole `export type { ... }` block
157
+ for (const rawEntry of match[2].split(",")) {
158
+ const entry = rawEntry.trim();
159
+ if (!entry || entry.startsWith("type ")) continue;
160
+ const asMatch = entry.match(/^\S+\s+as\s+(\S+)$/);
161
+ names.add(asMatch ? asMatch[1] : entry);
162
+ }
163
+ }
164
+ return [...names];
165
+ }
166
+
167
+ const nameCache = new Map<string, string[]>();
168
+
169
+ function resolveNames(spec: string): string[] {
170
+ const mod = SHARED_MODULES[spec];
171
+ if (mod.names) return mod.names;
172
+ if (!mod.parseNamesFrom) {
173
+ throw new Error(`[plugin-ui] ${spec} has neither "names" nor "parseNamesFrom"`);
174
+ }
175
+ if (!nameCache.has(spec)) {
176
+ const { specifier, extraNames = [] } = mod.parseNamesFrom;
177
+ const sourcePath = require.resolve(specifier);
178
+ const parsed = parseExportedValueNames(readFileSync(sourcePath, "utf8"));
179
+ nameCache.set(spec, [...new Set([...parsed, ...extraNames])]);
180
+ }
181
+ return nameCache.get(spec)!;
182
+ }
183
+
184
+ function shimSource(spec: string): string {
185
+ const mod = SHARED_MODULES[spec];
186
+ const names = resolveNames(spec);
187
+ const g = `(globalThis.__RB_SHARED__||{})[${JSON.stringify(spec)}]`;
188
+ return (
189
+ `const m = ${g};\n` +
190
+ `if (!m) throw new Error("[plugin-ui] host shared module not registered: ${spec}");\n` +
191
+ `export const { ${names.join(", ")} } = m;\n` +
192
+ (mod.hasDefault ? `export default (m.default ?? m);\n` : "")
193
+ );
194
+ }
195
+
196
+ const fileToSpec = Object.fromEntries(
197
+ Object.entries(SHARED_MODULES).map(([spec, { file }]) => [file, spec]),
198
+ );
199
+
200
+ function importMapScript(): string {
201
+ const imports: Record<string, string> = {};
202
+ for (const [spec, { file }] of Object.entries(SHARED_MODULES)) {
203
+ imports[spec] = `${SHARED_BASE}/${file}.js`;
204
+ }
205
+ return `<script type="importmap">${JSON.stringify({ imports })}</script>`;
206
+ }
207
+
208
+ /**
209
+ * Serves the shared-runtime shim modules at /plugin-shared/*.js and injects the
210
+ * import map so a dynamically-imported plugin's bare `import "react"` /
211
+ * `import "@rainbow-robotics/plugin-ui/react"` etc. resolve to the host's OWN
212
+ * instances (via window.__RB_SHARED__). Applied in all modes — any nexus build
213
+ * that can host plugins needs it.
214
+ */
215
+ export const pluginSharedRuntimePlugin = (): Plugin => ({
216
+ name: "plugin-ui-shared-runtime",
217
+ transformIndexHtml(html) {
218
+ return html.replace("</head>", ` ${importMapScript()}\n </head>`);
219
+ },
220
+ configureServer(server) {
221
+ server.middlewares.use((req, res, next) => {
222
+ const url = req.url?.split("?")[0];
223
+ if (!url?.startsWith(`${SHARED_BASE}/`)) return next();
224
+ const file = url.slice(`${SHARED_BASE}/`.length).replace(/\.js$/, "");
225
+ const spec = fileToSpec[file];
226
+ if (!spec) return next();
227
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
228
+ res.setHeader("Access-Control-Allow-Origin", "*");
229
+ res.end(shimSource(spec));
230
+ });
231
+ },
232
+ generateBundle() {
233
+ for (const [spec, { file }] of Object.entries(SHARED_MODULES)) {
234
+ this.emitFile({
235
+ type: "asset",
236
+ fileName: `plugin-shared/${file}.js`,
237
+ source: shimSource(spec),
238
+ });
239
+ }
240
+ },
241
+ });
242
+
243
+ /** Inject the preview-host runtime flags into index.html (plugin-preview only). */
244
+ export const pluginPreviewRuntimePlugin = (): Plugin => ({
245
+ name: "plugin-ui-preview-runtime",
246
+ apply: (_config, { mode }) => isPluginPreviewMode(mode),
247
+ transformIndexHtml(html) {
248
+ return html.replace(
249
+ "</head>",
250
+ [
251
+ " <script>",
252
+ " window.__RB_PLUGIN_PREVIEW_HOST__ = true;",
253
+ " window.__RB_ASSETS_BASE_URL__ = window.location.origin;",
254
+ " </script>",
255
+ " </head>",
256
+ ].join("\n"),
257
+ );
258
+ },
259
+ });
260
+
261
+ /** Serve the host's static assets (imgs/fonts/...) in plugin-preview dev mode. */
262
+ export const pluginPreviewAssetsPlugin = (opts: {
263
+ assetsDir: string;
264
+ prefixes?: string[];
265
+ }): Plugin => {
266
+ const { assetsDir, prefixes = defaultPreviewAssetPrefixes } = opts;
267
+ return {
268
+ name: "plugin-ui-preview-assets",
269
+ apply: (_config, { command, mode }) =>
270
+ command === "serve" && isPluginPreviewMode(mode),
271
+ configureServer(server) {
272
+ server.middlewares.use((req, res, next) => {
273
+ const url = req.url?.split("?")[0];
274
+ if (!url || !prefixes.some((prefix) => url.startsWith(prefix)))
275
+ return next();
276
+
277
+ const decodedUrl = decodeURIComponent(url);
278
+ const filePath = resolve(assetsDir, `.${decodedUrl}`);
279
+ if (!filePath.startsWith(assetsDir)) return next();
280
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) return next();
281
+
282
+ const contentType = contentTypeByExt[extname(filePath)];
283
+ if (contentType) res.setHeader("Content-Type", contentType);
284
+ res.setHeader("Access-Control-Allow-Origin", "*");
285
+ createReadStream(filePath).pipe(res);
286
+ });
287
+ },
288
+ };
289
+ };
290
+
291
+ /**
292
+ * All host-side plugin-ui Vite plugins in one call. Each self-gates by mode, so
293
+ * a host just spreads this into its `plugins` array.
294
+ *
295
+ * plugins: [muscatVitePlugin({ previewAssetsDir: resolve(here, "../assets/public") })]
296
+ */
297
+ export const muscatVitePlugin = (opts: {
298
+ /** Host static assets dir served in plugin-preview dev mode. */
299
+ previewAssetsDir: string;
300
+ /** Override preview asset URL prefixes. */
301
+ previewAssetPrefixes?: string[];
302
+ }): Plugin[] => [
303
+ pluginSharedRuntimePlugin(),
304
+ pluginPreviewRuntimePlugin(),
305
+ pluginPreviewAssetsPlugin({
306
+ assetsDir: opts.previewAssetsDir,
307
+ prefixes: opts.previewAssetPrefixes,
308
+ }),
309
+ ];