@fluid-app/portal-sdk 0.1.126 → 0.1.127
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/dist/vite/index.cjs +75 -0
- package/dist/vite/index.cjs.map +1 -1
- package/dist/vite/index.d.cts +13 -1
- package/dist/vite/index.d.cts.map +1 -1
- package/dist/vite/index.d.mts +13 -1
- package/dist/vite/index.d.mts.map +1 -1
- package/dist/vite/index.mjs +76 -2
- package/dist/vite/index.mjs.map +1 -1
- package/package.json +20 -14
package/dist/vite/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
require("../chunk-9hOWP6kD.cjs");
|
|
3
3
|
let node_path = require("node:path");
|
|
4
4
|
let node_fs = require("node:fs");
|
|
5
|
+
let node_fs_promises = require("node:fs/promises");
|
|
5
6
|
//#region src/vite/validate-manifest.ts
|
|
6
7
|
/**
|
|
7
8
|
* Lightweight manifest validation for the SDK vite plugin.
|
|
@@ -256,7 +257,81 @@ async function loadManifests(server, logger, configFilePath) {
|
|
|
256
257
|
return manifests.map(({ component: _component, ...rest }) => rest);
|
|
257
258
|
}
|
|
258
259
|
//#endregion
|
|
260
|
+
//#region ../core/src/preview/protocol.ts
|
|
261
|
+
const ALLOWED_ORIGIN_PATTERNS = [
|
|
262
|
+
/^https?:\/\/localhost(:\d+)?$/,
|
|
263
|
+
/^https?:\/\/127\.0\.0\.1(:\d+)?$/,
|
|
264
|
+
/^https:\/\/[a-z0-9-]+\.portal\.fluid\.app$/,
|
|
265
|
+
/^https:\/\/[a-z0-9-]+\.fluid\.app$/
|
|
266
|
+
];
|
|
267
|
+
/**
|
|
268
|
+
* Validate that a postMessage origin is trusted.
|
|
269
|
+
* Accepts localhost (any port), *.portal.fluid.app, and *.fluid.app.
|
|
270
|
+
*/
|
|
271
|
+
function isAllowedOrigin(origin) {
|
|
272
|
+
return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/vite/preview-plugin.ts
|
|
276
|
+
/**
|
|
277
|
+
* Vite plugin that serves the widget preview route.
|
|
278
|
+
*
|
|
279
|
+
* Dev mode: serves `/__preview__` by transforming preview.html through
|
|
280
|
+
* Vite's HTML pipeline (so import resolution and HMR work).
|
|
281
|
+
*
|
|
282
|
+
* Build mode: no-op — the production preview.html is handled by adding
|
|
283
|
+
* it as a second Rollup input in vite.config.ts.
|
|
284
|
+
*/
|
|
285
|
+
function fluidPreviewPlugin() {
|
|
286
|
+
return {
|
|
287
|
+
name: "fluid-preview-plugin",
|
|
288
|
+
configureServer(server) {
|
|
289
|
+
server.middlewares.use("/__preview__", async (req, res) => {
|
|
290
|
+
try {
|
|
291
|
+
const htmlPath = (0, node_path.resolve)(server.config.root, "preview.html");
|
|
292
|
+
let html;
|
|
293
|
+
try {
|
|
294
|
+
html = await (0, node_fs_promises.readFile)(htmlPath, "utf-8");
|
|
295
|
+
} catch {
|
|
296
|
+
html = getDefaultPreviewHtml();
|
|
297
|
+
}
|
|
298
|
+
html = await server.transformIndexHtml("/__preview__", html);
|
|
299
|
+
res.setHeader("Content-Type", "text/html");
|
|
300
|
+
const origin = req.headers.origin;
|
|
301
|
+
if (origin && isAllowedOrigin(origin)) {
|
|
302
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
303
|
+
res.setHeader("Vary", "Origin");
|
|
304
|
+
}
|
|
305
|
+
res.statusCode = 200;
|
|
306
|
+
res.end(html);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
server.config.logger.error(`[fluid] Failed to serve preview: ${err}`);
|
|
309
|
+
res.statusCode = 500;
|
|
310
|
+
res.end("Preview failed to load");
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function getDefaultPreviewHtml() {
|
|
317
|
+
return `<!doctype html>
|
|
318
|
+
<html lang="en">
|
|
319
|
+
<head>
|
|
320
|
+
<meta charset="UTF-8" />
|
|
321
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
322
|
+
<title>Widget Preview</title>
|
|
323
|
+
<meta name="referrer" content="strict-origin-when-cross-origin" />
|
|
324
|
+
<style>body { margin: 0; padding: 0; }</style>
|
|
325
|
+
</head>
|
|
326
|
+
<body>
|
|
327
|
+
<div id="preview-root"></div>
|
|
328
|
+
<script type="module" src="/src/preview-entry.tsx"><\/script>
|
|
329
|
+
</body>
|
|
330
|
+
</html>`;
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
259
333
|
exports.fluidBuilderPreviewPlugin = fluidBuilderPreviewPlugin;
|
|
260
334
|
exports.fluidManifestPlugin = fluidManifestPlugin;
|
|
335
|
+
exports.fluidPreviewPlugin = fluidPreviewPlugin;
|
|
261
336
|
|
|
262
337
|
//# sourceMappingURL=index.cjs.map
|
package/dist/vite/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/vite/validate-manifest.ts","../../src/vite/builder-preview-plugin.ts","../../src/vite/manifest-plugin.ts"],"sourcesContent":["/**\n * Lightweight manifest validation for the SDK vite plugin.\n *\n * Inlined here (rather than imported from @fluid-app/portal-core) because\n * portal-core is private and not published to npm. This avoids a runtime\n * ERR_MODULE_NOT_FOUND for portals installed from npm.\n */\n\nconst VALID_FIELD_TYPES = [\n \"text\",\n \"textarea\",\n \"number\",\n \"boolean\",\n \"select\",\n \"color\",\n \"range\",\n \"dataSource\",\n \"resource\",\n \"image\",\n \"alignment\",\n \"slider\",\n \"colorPicker\",\n \"sectionHeader\",\n \"separator\",\n \"buttonGroup\",\n \"colorSelect\",\n \"sectionLayoutSelect\",\n \"background\",\n \"contentPosition\",\n \"textSizeSelect\",\n \"cssUnit\",\n \"fontPicker\",\n \"stringArray\",\n \"borderRadius\",\n \"screenPicker\",\n] as const;\n\ninterface ValidationError {\n path: string;\n message: string;\n}\n\ntype ValidationResult =\n | { success: true }\n | { success: false; errors: ValidationError[] };\n\nexport function validateManifest(input: unknown): ValidationResult {\n const errors: ValidationError[] = [];\n const m = input as Record<string, unknown>;\n\n if (!m || typeof m !== \"object\") {\n return {\n success: false,\n errors: [{ path: \"\", message: \"Manifest must be an object\" }],\n };\n }\n\n // Required string fields\n for (const key of [\n \"type\",\n \"displayName\",\n \"description\",\n \"icon\",\n \"category\",\n ]) {\n if (typeof m[key] !== \"string\" || (m[key] as string).length === 0) {\n errors.push({\n path: key,\n message: `${key} is required and must be a non-empty string`,\n });\n }\n }\n\n if (typeof m.manifestVersion !== \"number\" || m.manifestVersion < 1) {\n errors.push({\n path: \"manifestVersion\",\n message: \"manifestVersion must be a positive integer\",\n });\n }\n\n if (typeof m.component !== \"function\") {\n errors.push({\n path: \"component\",\n message: \"component must be a React component (function)\",\n });\n }\n\n // Property schema validation\n const schema = m.propertySchema as Record<string, unknown> | undefined;\n if (schema && typeof schema === \"object\") {\n if (typeof schema.widgetType !== \"string\" || !schema.widgetType) {\n errors.push({\n path: \"propertySchema.widgetType\",\n message: \"widgetType is required\",\n });\n }\n if (typeof m.type === \"string\" && schema.widgetType !== m.type) {\n errors.push({\n path: \"propertySchema.widgetType\",\n message: \"manifest.type must match manifest.propertySchema.widgetType\",\n });\n }\n if (Array.isArray(schema.fields)) {\n for (let i = 0; i < schema.fields.length; i++) {\n const field = schema.fields[i] as Record<string, unknown>;\n if (!field || typeof field.type !== \"string\") continue;\n if (\n !VALID_FIELD_TYPES.includes(\n field.type as (typeof VALID_FIELD_TYPES)[number],\n )\n ) {\n errors.push({\n path: `propertySchema.fields.${i}.type`,\n message: `Invalid field type \"${field.type}\". Valid types: ${VALID_FIELD_TYPES.join(\", \")}`,\n });\n }\n }\n }\n }\n\n return errors.length === 0 ? { success: true } : { success: false, errors };\n}\n","import type { Plugin, ResolvedConfig } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nconst VIRTUAL_ENTRY_ID = \"virtual:builder-preview-entry\";\nconst RESOLVED_VIRTUAL_ID = \"\\0\" + VIRTUAL_ENTRY_ID;\n\nconst RAW_HTML = `<!doctype html>\n<html lang=\"en\" data-theme-mode=\"dark\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Custom Widget Preview</title>\n <style>\n body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }\n </style>\n </head>\n <body>\n <div id=\"builder-preview-root\"></div>\n <script type=\"module\" src=\"/@id/virtual:builder-preview-entry\"></script>\n </body>\n</html>`;\n\n/**\n * Vite plugin that serves a standalone widget preview page at /builder-preview.\n *\n * Dev mode only. Renders all customWidgets from portal.config.ts with\n * a live preview and property editor — no auth, no iframe, no fluid-admin needed.\n */\nexport function fluidBuilderPreviewPlugin(): Plugin {\n let configPath: string;\n let cssPath: string;\n\n return {\n name: \"fluid-builder-preview\",\n apply: \"serve\",\n\n configResolved(config: ResolvedConfig) {\n const root = config.root;\n const candidates = [\"src/portal.config.ts\", \"portal.config.ts\"];\n configPath =\n candidates.find((c) => existsSync(join(root, c))) ??\n \"src/portal.config.ts\";\n const cssCandidates = [\n \"src/index.css\",\n \"src/styles/index.css\",\n \"index.css\",\n ];\n cssPath =\n cssCandidates.find((c) => existsSync(join(root, c))) ?? \"src/index.css\";\n },\n\n resolveId(id) {\n if (id === VIRTUAL_ENTRY_ID) return RESOLVED_VIRTUAL_ID;\n },\n\n load(id) {\n if (id === RESOLVED_VIRTUAL_ID) {\n return `\nimport \"/${cssPath}\";\nimport * as portalConfig from \"/${configPath}\";\nimport { createRoot } from \"react-dom/client\";\nimport { createElement } from \"react\";\nimport { BuilderPreviewApp } from \"@fluid-app/portal-preview\";\n\nconst widgets = portalConfig.customWidgets || [];\nconst root = document.getElementById(\"builder-preview-root\");\nif (root) {\n createRoot(root).render(\n createElement(BuilderPreviewApp, { widgets })\n );\n}\n`;\n }\n },\n\n configureServer(server) {\n server.middlewares.use(async (req, res, next) => {\n const pathname = (req.url ?? \"\").split(\"?\")[0];\n if (pathname !== \"/builder-preview\" && pathname !== \"/builder-preview/\")\n return next();\n try {\n const transformed = await server.transformIndexHtml(\n \"/builder-preview\",\n RAW_HTML,\n );\n res.setHeader(\"Content-Type\", \"text/html\");\n res.end(transformed);\n } catch (e) {\n server.config.logger.error(\n `[fluid] Failed to serve builder preview: ${e}`,\n );\n res.statusCode = 500;\n res.end(\"Builder preview failed to load\");\n }\n });\n },\n };\n}\n","import type { Plugin, ResolvedConfig, ViteDevServer, Logger } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateManifest } from \"./validate-manifest\";\nimport { fluidBuilderPreviewPlugin } from \"./builder-preview-plugin\";\n\n/**\n * Vite plugin bundle that serves widget manifest metadata and the builder preview.\n *\n * Returns an array of plugins:\n * 1. Manifest plugin — serves /__manifests__ (dev) and emits __manifests__.json (build)\n * 2. Builder preview plugin — serves /builder-preview with live widget editing (dev only)\n *\n * Every portal using `fluidManifestPlugin()` automatically gets the builder preview.\n */\nexport function fluidManifestPlugin(): Plugin[] {\n return [fluidManifestPluginInternal(), fluidBuilderPreviewPlugin()];\n}\n\nfunction fluidManifestPluginInternal(): Plugin {\n let configPath: string;\n\n return {\n name: \"fluid-manifest-plugin\",\n\n configResolved(config: ResolvedConfig) {\n const root = config.root;\n const candidates = [\n \"src/widgets.config.ts\",\n \"src/portal.config.ts\",\n \"portal.config.ts\",\n ];\n configPath =\n \"/\" +\n (candidates.find((c) => existsSync(join(root, c))) ??\n \"src/portal.config.ts\");\n },\n\n configureServer(server) {\n server.middlewares.use(\"/__manifests__\", async (_req, res) => {\n try {\n const serializable = await loadManifests(\n server,\n server.config.logger,\n configPath,\n );\n\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.end(JSON.stringify(serializable));\n } catch (err) {\n server.config.logger.error(\n `[fluid] Failed to load manifests: ${err}`,\n );\n res.statusCode = 500;\n res.end(JSON.stringify({ error: String(err) }));\n }\n });\n },\n\n generateBundle() {\n // Build mode: emit placeholder. The CLI extraction utility handles\n // actual build-time manifest extraction via tsx subprocess.\n this.warn(\n \"[fluid] fluidManifestPlugin: emitting empty __manifests__.json. \" +\n \"Run `fluid build` instead of `vite build` to include widget manifests.\",\n );\n this.emitFile({\n type: \"asset\",\n fileName: \"__manifests__.json\",\n source: JSON.stringify([]),\n });\n },\n };\n}\n\n/**\n * Load and serialize manifests from the resolved widget config\n * (widgets.config.ts or portal.config.ts) via Vite's ssrLoadModule.\n * Validates each manifest before stripping the `component` field.\n * Returns an empty array if the config module or export is missing/invalid.\n */\nasync function loadManifests(\n server: ViteDevServer,\n logger?: Logger,\n configFilePath?: string,\n): Promise<unknown[]> {\n let mod: Record<string, unknown>;\n try {\n mod = await server.ssrLoadModule(configFilePath ?? \"/src/portal.config.ts\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const isSSRError =\n /(document|window|navigator|localStorage|sessionStorage|location|history|HTMLElement) is not defined/.test(\n message,\n );\n\n if (isSSRError) {\n const configFile = configFilePath ?? \"/src/portal.config.ts\";\n const isPortalConfig = configFile.includes(\"portal.config\");\n const fixHint = isPortalConfig\n ? ` Fix: Create src/widgets.config.ts with only your customWidgets export.\\n` +\n ` The manifest plugin will load it instead of portal.config.ts.\\n`\n : ` Fix: Ensure ${configFile} does not import browser-only code.\\n`;\n\n logger?.warn(\n `[fluid] Cannot load widget manifests — ${configFile} imports ` +\n `browser-only code that fails during server-side evaluation.\\n` +\n ` Custom widgets will not appear in the builder.\\n` +\n fixHint +\n ` Widget components are fine — the issue is usually screen imports\\n` +\n ` (e.g. DashboardScreen) that pull in the SDK barrel export.`,\n );\n } else {\n logger?.warn(`[fluid] Could not load widget config: ${message}`);\n }\n return [];\n }\n\n const rawWidgets = mod.customWidgets;\n if (!rawWidgets) {\n if (configFilePath?.includes(\"widgets.config\")) {\n logger?.warn(\n `[fluid] widgets.config.ts was loaded but exports no customWidgets. ` +\n `Custom widgets will not appear in the builder.`,\n );\n }\n return [];\n }\n\n if (!Array.isArray(rawWidgets)) {\n logger?.warn(\n `[fluid] customWidgets export is not an array (got ${typeof rawWidgets}). Skipping manifest serving.`,\n );\n return [];\n }\n\n const manifests = rawWidgets as Record<string, unknown>[];\n\n // Validate full manifests (with component) before stripping\n if (logger) {\n for (const manifest of manifests) {\n const result = validateManifest(manifest);\n if (!result.success) {\n const type = (manifest as { type?: string }).type ?? \"unknown\";\n logger.warn(\n `[fluid] Invalid manifest for \"${type}\":\\n` +\n result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\"),\n );\n }\n }\n }\n\n return manifests.map(\n ({ component: _component, ...rest }: Record<string, unknown>) => rest,\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/vite/validate-manifest.ts","../../src/vite/builder-preview-plugin.ts","../../src/vite/manifest-plugin.ts","../../../core/src/preview/protocol.ts","../../src/vite/preview-plugin.ts"],"sourcesContent":["/**\n * Lightweight manifest validation for the SDK vite plugin.\n *\n * Inlined here (rather than imported from @fluid-app/portal-core) because\n * portal-core is private and not published to npm. This avoids a runtime\n * ERR_MODULE_NOT_FOUND for portals installed from npm.\n */\n\nconst VALID_FIELD_TYPES = [\n \"text\",\n \"textarea\",\n \"number\",\n \"boolean\",\n \"select\",\n \"color\",\n \"range\",\n \"dataSource\",\n \"resource\",\n \"image\",\n \"alignment\",\n \"slider\",\n \"colorPicker\",\n \"sectionHeader\",\n \"separator\",\n \"buttonGroup\",\n \"colorSelect\",\n \"sectionLayoutSelect\",\n \"background\",\n \"contentPosition\",\n \"textSizeSelect\",\n \"cssUnit\",\n \"fontPicker\",\n \"stringArray\",\n \"borderRadius\",\n \"screenPicker\",\n] as const;\n\ninterface ValidationError {\n path: string;\n message: string;\n}\n\ntype ValidationResult =\n | { success: true }\n | { success: false; errors: ValidationError[] };\n\nexport function validateManifest(input: unknown): ValidationResult {\n const errors: ValidationError[] = [];\n const m = input as Record<string, unknown>;\n\n if (!m || typeof m !== \"object\") {\n return {\n success: false,\n errors: [{ path: \"\", message: \"Manifest must be an object\" }],\n };\n }\n\n // Required string fields\n for (const key of [\n \"type\",\n \"displayName\",\n \"description\",\n \"icon\",\n \"category\",\n ]) {\n if (typeof m[key] !== \"string\" || (m[key] as string).length === 0) {\n errors.push({\n path: key,\n message: `${key} is required and must be a non-empty string`,\n });\n }\n }\n\n if (typeof m.manifestVersion !== \"number\" || m.manifestVersion < 1) {\n errors.push({\n path: \"manifestVersion\",\n message: \"manifestVersion must be a positive integer\",\n });\n }\n\n if (typeof m.component !== \"function\") {\n errors.push({\n path: \"component\",\n message: \"component must be a React component (function)\",\n });\n }\n\n // Property schema validation\n const schema = m.propertySchema as Record<string, unknown> | undefined;\n if (schema && typeof schema === \"object\") {\n if (typeof schema.widgetType !== \"string\" || !schema.widgetType) {\n errors.push({\n path: \"propertySchema.widgetType\",\n message: \"widgetType is required\",\n });\n }\n if (typeof m.type === \"string\" && schema.widgetType !== m.type) {\n errors.push({\n path: \"propertySchema.widgetType\",\n message: \"manifest.type must match manifest.propertySchema.widgetType\",\n });\n }\n if (Array.isArray(schema.fields)) {\n for (let i = 0; i < schema.fields.length; i++) {\n const field = schema.fields[i] as Record<string, unknown>;\n if (!field || typeof field.type !== \"string\") continue;\n if (\n !VALID_FIELD_TYPES.includes(\n field.type as (typeof VALID_FIELD_TYPES)[number],\n )\n ) {\n errors.push({\n path: `propertySchema.fields.${i}.type`,\n message: `Invalid field type \"${field.type}\". Valid types: ${VALID_FIELD_TYPES.join(\", \")}`,\n });\n }\n }\n }\n }\n\n return errors.length === 0 ? { success: true } : { success: false, errors };\n}\n","import type { Plugin, ResolvedConfig } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nconst VIRTUAL_ENTRY_ID = \"virtual:builder-preview-entry\";\nconst RESOLVED_VIRTUAL_ID = \"\\0\" + VIRTUAL_ENTRY_ID;\n\nconst RAW_HTML = `<!doctype html>\n<html lang=\"en\" data-theme-mode=\"dark\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Custom Widget Preview</title>\n <style>\n body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }\n </style>\n </head>\n <body>\n <div id=\"builder-preview-root\"></div>\n <script type=\"module\" src=\"/@id/virtual:builder-preview-entry\"></script>\n </body>\n</html>`;\n\n/**\n * Vite plugin that serves a standalone widget preview page at /builder-preview.\n *\n * Dev mode only. Renders all customWidgets from portal.config.ts with\n * a live preview and property editor — no auth, no iframe, no fluid-admin needed.\n */\nexport function fluidBuilderPreviewPlugin(): Plugin {\n let configPath: string;\n let cssPath: string;\n\n return {\n name: \"fluid-builder-preview\",\n apply: \"serve\",\n\n configResolved(config: ResolvedConfig) {\n const root = config.root;\n const candidates = [\"src/portal.config.ts\", \"portal.config.ts\"];\n configPath =\n candidates.find((c) => existsSync(join(root, c))) ??\n \"src/portal.config.ts\";\n const cssCandidates = [\n \"src/index.css\",\n \"src/styles/index.css\",\n \"index.css\",\n ];\n cssPath =\n cssCandidates.find((c) => existsSync(join(root, c))) ?? \"src/index.css\";\n },\n\n resolveId(id) {\n if (id === VIRTUAL_ENTRY_ID) return RESOLVED_VIRTUAL_ID;\n },\n\n load(id) {\n if (id === RESOLVED_VIRTUAL_ID) {\n return `\nimport \"/${cssPath}\";\nimport * as portalConfig from \"/${configPath}\";\nimport { createRoot } from \"react-dom/client\";\nimport { createElement } from \"react\";\nimport { BuilderPreviewApp } from \"@fluid-app/portal-preview\";\n\nconst widgets = portalConfig.customWidgets || [];\nconst root = document.getElementById(\"builder-preview-root\");\nif (root) {\n createRoot(root).render(\n createElement(BuilderPreviewApp, { widgets })\n );\n}\n`;\n }\n },\n\n configureServer(server) {\n server.middlewares.use(async (req, res, next) => {\n const pathname = (req.url ?? \"\").split(\"?\")[0];\n if (pathname !== \"/builder-preview\" && pathname !== \"/builder-preview/\")\n return next();\n try {\n const transformed = await server.transformIndexHtml(\n \"/builder-preview\",\n RAW_HTML,\n );\n res.setHeader(\"Content-Type\", \"text/html\");\n res.end(transformed);\n } catch (e) {\n server.config.logger.error(\n `[fluid] Failed to serve builder preview: ${e}`,\n );\n res.statusCode = 500;\n res.end(\"Builder preview failed to load\");\n }\n });\n },\n };\n}\n","import type { Plugin, ResolvedConfig, ViteDevServer, Logger } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateManifest } from \"./validate-manifest\";\nimport { fluidBuilderPreviewPlugin } from \"./builder-preview-plugin\";\n\n/**\n * Vite plugin bundle that serves widget manifest metadata and the builder preview.\n *\n * Returns an array of plugins:\n * 1. Manifest plugin — serves /__manifests__ (dev) and emits __manifests__.json (build)\n * 2. Builder preview plugin — serves /builder-preview with live widget editing (dev only)\n *\n * Every portal using `fluidManifestPlugin()` automatically gets the builder preview.\n */\nexport function fluidManifestPlugin(): Plugin[] {\n return [fluidManifestPluginInternal(), fluidBuilderPreviewPlugin()];\n}\n\nfunction fluidManifestPluginInternal(): Plugin {\n let configPath: string;\n\n return {\n name: \"fluid-manifest-plugin\",\n\n configResolved(config: ResolvedConfig) {\n const root = config.root;\n const candidates = [\n \"src/widgets.config.ts\",\n \"src/portal.config.ts\",\n \"portal.config.ts\",\n ];\n configPath =\n \"/\" +\n (candidates.find((c) => existsSync(join(root, c))) ??\n \"src/portal.config.ts\");\n },\n\n configureServer(server) {\n server.middlewares.use(\"/__manifests__\", async (_req, res) => {\n try {\n const serializable = await loadManifests(\n server,\n server.config.logger,\n configPath,\n );\n\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.end(JSON.stringify(serializable));\n } catch (err) {\n server.config.logger.error(\n `[fluid] Failed to load manifests: ${err}`,\n );\n res.statusCode = 500;\n res.end(JSON.stringify({ error: String(err) }));\n }\n });\n },\n\n generateBundle() {\n // Build mode: emit placeholder. The CLI extraction utility handles\n // actual build-time manifest extraction via tsx subprocess.\n this.warn(\n \"[fluid] fluidManifestPlugin: emitting empty __manifests__.json. \" +\n \"Run `fluid build` instead of `vite build` to include widget manifests.\",\n );\n this.emitFile({\n type: \"asset\",\n fileName: \"__manifests__.json\",\n source: JSON.stringify([]),\n });\n },\n };\n}\n\n/**\n * Load and serialize manifests from the resolved widget config\n * (widgets.config.ts or portal.config.ts) via Vite's ssrLoadModule.\n * Validates each manifest before stripping the `component` field.\n * Returns an empty array if the config module or export is missing/invalid.\n */\nasync function loadManifests(\n server: ViteDevServer,\n logger?: Logger,\n configFilePath?: string,\n): Promise<unknown[]> {\n let mod: Record<string, unknown>;\n try {\n mod = await server.ssrLoadModule(configFilePath ?? \"/src/portal.config.ts\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const isSSRError =\n /(document|window|navigator|localStorage|sessionStorage|location|history|HTMLElement) is not defined/.test(\n message,\n );\n\n if (isSSRError) {\n const configFile = configFilePath ?? \"/src/portal.config.ts\";\n const isPortalConfig = configFile.includes(\"portal.config\");\n const fixHint = isPortalConfig\n ? ` Fix: Create src/widgets.config.ts with only your customWidgets export.\\n` +\n ` The manifest plugin will load it instead of portal.config.ts.\\n`\n : ` Fix: Ensure ${configFile} does not import browser-only code.\\n`;\n\n logger?.warn(\n `[fluid] Cannot load widget manifests — ${configFile} imports ` +\n `browser-only code that fails during server-side evaluation.\\n` +\n ` Custom widgets will not appear in the builder.\\n` +\n fixHint +\n ` Widget components are fine — the issue is usually screen imports\\n` +\n ` (e.g. DashboardScreen) that pull in the SDK barrel export.`,\n );\n } else {\n logger?.warn(`[fluid] Could not load widget config: ${message}`);\n }\n return [];\n }\n\n const rawWidgets = mod.customWidgets;\n if (!rawWidgets) {\n if (configFilePath?.includes(\"widgets.config\")) {\n logger?.warn(\n `[fluid] widgets.config.ts was loaded but exports no customWidgets. ` +\n `Custom widgets will not appear in the builder.`,\n );\n }\n return [];\n }\n\n if (!Array.isArray(rawWidgets)) {\n logger?.warn(\n `[fluid] customWidgets export is not an array (got ${typeof rawWidgets}). Skipping manifest serving.`,\n );\n return [];\n }\n\n const manifests = rawWidgets as Record<string, unknown>[];\n\n // Validate full manifests (with component) before stripping\n if (logger) {\n for (const manifest of manifests) {\n const result = validateManifest(manifest);\n if (!result.success) {\n const type = (manifest as { type?: string }).type ?? \"unknown\";\n logger.warn(\n `[fluid] Invalid manifest for \"${type}\":\\n` +\n result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\"),\n );\n }\n }\n }\n\n return manifests.map(\n ({ component: _component, ...rest }: Record<string, unknown>) => rest,\n );\n}\n","/**\n * PostMessage protocol for builder ↔ iframe widget preview communication.\n *\n * Four-step handshake:\n * 1. Iframe sends `ready` once mounted\n * 2. Builder sends `auth` with token (NEVER via URL)\n * 3. Iframe sends `auth-ready` once FluidProvider settles\n * 4. Builder sends `render-widget` with type + props\n *\n * This prevents the race condition where render-widget arrives\n * before the provider stack has initialized with the auth token.\n */\n\n// ---------------------------------------------------------------------------\n// Builder → Iframe commands\n// ---------------------------------------------------------------------------\n\nexport interface AuthCommand {\n type: \"auth\";\n token: string;\n}\n\nexport interface RenderWidgetCommand {\n type: \"render-widget\";\n widgetType: string;\n props: Record<string, unknown>;\n}\n\nexport interface UnmountWidgetCommand {\n type: \"unmount-widget\";\n}\n\nexport interface SelectWidgetCommand {\n type: \"select-widget\";\n widgetType: string;\n}\n\nexport type PreviewCommand =\n | AuthCommand\n | RenderWidgetCommand\n | UnmountWidgetCommand\n | SelectWidgetCommand;\n\n// ---------------------------------------------------------------------------\n// Iframe → Builder events\n// ---------------------------------------------------------------------------\n\nexport interface ReadyEvent {\n type: \"ready\";\n}\n\nexport interface AuthReadyEvent {\n type: \"auth-ready\";\n}\n\nexport interface WidgetRenderedEvent {\n type: \"widget-rendered\";\n widgetType: string;\n dimensions: { width: number; height: number };\n}\n\nexport interface WidgetErrorEvent {\n type: \"widget-error\";\n widgetType: string;\n error: string;\n}\n\nexport interface WidgetClickedEvent {\n type: \"widget-clicked\";\n widgetType: string;\n}\n\nexport interface WidgetHmrEvent {\n type: \"widget-hmr\";\n}\n\nexport type PreviewEvent =\n | ReadyEvent\n | AuthReadyEvent\n | WidgetRenderedEvent\n | WidgetErrorEvent\n | WidgetClickedEvent\n | WidgetHmrEvent;\n\n// ---------------------------------------------------------------------------\n// Origin validation\n// ---------------------------------------------------------------------------\n\nconst ALLOWED_ORIGIN_PATTERNS = [\n /^https?:\\/\\/localhost(:\\d+)?$/,\n /^https?:\\/\\/127\\.0\\.0\\.1(:\\d+)?$/,\n /^https:\\/\\/[a-z0-9-]+\\.portal\\.fluid\\.app$/,\n /^https:\\/\\/[a-z0-9-]+\\.fluid\\.app$/,\n];\n\n/**\n * Validate that a postMessage origin is trusted.\n * Accepts localhost (any port), *.portal.fluid.app, and *.fluid.app.\n */\nexport function isAllowedOrigin(origin: string): boolean {\n return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));\n}\n\n// ---------------------------------------------------------------------------\n// Type guards\n// ---------------------------------------------------------------------------\n\n/** Check if a message is a valid PreviewCommand (builder → iframe). */\nexport function isPreviewCommand(data: unknown): data is PreviewCommand {\n if (typeof data !== \"object\" || data === null) return false;\n const msg = data as Record<string, unknown>;\n switch (msg[\"type\"]) {\n case \"auth\":\n return typeof msg[\"token\"] === \"string\";\n case \"render-widget\":\n return (\n typeof msg[\"widgetType\"] === \"string\" &&\n typeof msg[\"props\"] === \"object\" &&\n msg[\"props\"] !== null &&\n !Array.isArray(msg[\"props\"])\n );\n case \"unmount-widget\":\n return true;\n case \"select-widget\":\n return typeof msg[\"widgetType\"] === \"string\";\n default:\n return false;\n }\n}\n\n/** Check if a message is a valid PreviewEvent (iframe → builder). */\nexport function isPreviewEvent(data: unknown): data is PreviewEvent {\n if (typeof data !== \"object\" || data === null) return false;\n const msg = data as Record<string, unknown>;\n switch (msg[\"type\"]) {\n case \"ready\":\n case \"auth-ready\":\n case \"widget-hmr\":\n return true;\n case \"widget-rendered\":\n return (\n typeof msg[\"widgetType\"] === \"string\" &&\n typeof msg[\"dimensions\"] === \"object\" &&\n msg[\"dimensions\"] !== null &&\n Number.isFinite(\n (msg[\"dimensions\"] as Record<string, unknown>)[\"width\"],\n ) &&\n Number.isFinite(\n (msg[\"dimensions\"] as Record<string, unknown>)[\"height\"],\n )\n );\n case \"widget-error\":\n return (\n typeof msg[\"widgetType\"] === \"string\" &&\n typeof msg[\"error\"] === \"string\"\n );\n case \"widget-clicked\":\n return typeof msg[\"widgetType\"] === \"string\";\n default:\n return false;\n }\n}\n","import { resolve } from \"node:path\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Plugin } from \"vite\";\nimport { isAllowedOrigin } from \"@fluid-app/portal-core/preview/protocol\";\n\n/**\n * Vite plugin that serves the widget preview route.\n *\n * Dev mode: serves `/__preview__` by transforming preview.html through\n * Vite's HTML pipeline (so import resolution and HMR work).\n *\n * Build mode: no-op — the production preview.html is handled by adding\n * it as a second Rollup input in vite.config.ts.\n */\nexport function fluidPreviewPlugin(): Plugin {\n return {\n name: \"fluid-preview-plugin\",\n\n configureServer(server) {\n // Serve the preview HTML shell at /__preview__\n server.middlewares.use(\"/__preview__\", async (req, res) => {\n try {\n // Read the preview.html from the project root (copied from template)\n const htmlPath = resolve(server.config.root, \"preview.html\");\n let html: string;\n try {\n html = await readFile(htmlPath, \"utf-8\");\n } catch {\n // Fallback: serve a minimal preview shell\n html = getDefaultPreviewHtml();\n }\n\n // Transform through Vite's HTML pipeline for HMR + import resolution\n html = await server.transformIndexHtml(\"/__preview__\", html);\n\n res.setHeader(\"Content-Type\", \"text/html\");\n const origin = req.headers.origin;\n if (origin && isAllowedOrigin(origin)) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n res.setHeader(\"Vary\", \"Origin\");\n }\n res.statusCode = 200;\n res.end(html);\n } catch (err) {\n server.config.logger.error(`[fluid] Failed to serve preview: ${err}`);\n res.statusCode = 500;\n res.end(\"Preview failed to load\");\n }\n });\n },\n };\n}\n\nfunction getDefaultPreviewHtml(): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Widget Preview</title>\n <meta name=\"referrer\" content=\"strict-origin-when-cross-origin\" />\n <style>body { margin: 0; padding: 0; }</style>\n </head>\n <body>\n <div id=\"preview-root\"></div>\n <script type=\"module\" src=\"/src/preview-entry.tsx\"></script>\n </body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;;AAQA,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAWD,SAAgB,iBAAiB,OAAkC;CACjE,MAAM,SAA4B,EAAE;CACpC,MAAM,IAAI;AAEV,KAAI,CAAC,KAAK,OAAO,MAAM,SACrB,QAAO;EACL,SAAS;EACT,QAAQ,CAAC;GAAE,MAAM;GAAI,SAAS;GAA8B,CAAC;EAC9D;AAIH,MAAK,MAAM,OAAO;EAChB;EACA;EACA;EACA;EACA;EACD,CACC,KAAI,OAAO,EAAE,SAAS,YAAa,EAAE,KAAgB,WAAW,EAC9D,QAAO,KAAK;EACV,MAAM;EACN,SAAS,GAAG,IAAI;EACjB,CAAC;AAIN,KAAI,OAAO,EAAE,oBAAoB,YAAY,EAAE,kBAAkB,EAC/D,QAAO,KAAK;EACV,MAAM;EACN,SAAS;EACV,CAAC;AAGJ,KAAI,OAAO,EAAE,cAAc,WACzB,QAAO,KAAK;EACV,MAAM;EACN,SAAS;EACV,CAAC;CAIJ,MAAM,SAAS,EAAE;AACjB,KAAI,UAAU,OAAO,WAAW,UAAU;AACxC,MAAI,OAAO,OAAO,eAAe,YAAY,CAAC,OAAO,WACnD,QAAO,KAAK;GACV,MAAM;GACN,SAAS;GACV,CAAC;AAEJ,MAAI,OAAO,EAAE,SAAS,YAAY,OAAO,eAAe,EAAE,KACxD,QAAO,KAAK;GACV,MAAM;GACN,SAAS;GACV,CAAC;AAEJ,MAAI,MAAM,QAAQ,OAAO,OAAO,CAC9B,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,OAAO,QAAQ,KAAK;GAC7C,MAAM,QAAQ,OAAO,OAAO;AAC5B,OAAI,CAAC,SAAS,OAAO,MAAM,SAAS,SAAU;AAC9C,OACE,CAAC,kBAAkB,SACjB,MAAM,KACP,CAED,QAAO,KAAK;IACV,MAAM,yBAAyB,EAAE;IACjC,SAAS,uBAAuB,MAAM,KAAK,kBAAkB,kBAAkB,KAAK,KAAK;IAC1F,CAAC;;;AAMV,QAAO,OAAO,WAAW,IAAI,EAAE,SAAS,MAAM,GAAG;EAAE,SAAS;EAAO;EAAQ;;;;ACpH7E,MAAM,mBAAmB;AACzB,MAAM,sBAAsB,OAAO;AAEnC,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;AAsBjB,SAAgB,4BAAoC;CAClD,IAAI;CACJ,IAAI;AAEJ,QAAO;EACL,MAAM;EACN,OAAO;EAEP,eAAe,QAAwB;GACrC,MAAM,OAAO,OAAO;AAEpB,gBADmB,CAAC,wBAAwB,mBAAmB,CAElD,MAAM,OAAA,GAAA,QAAA,aAAA,GAAA,UAAA,MAAsB,MAAM,EAAE,CAAC,CAAC,IACjD;AAMF,aALsB;IACpB;IACA;IACA;IACD,CAEe,MAAM,OAAA,GAAA,QAAA,aAAA,GAAA,UAAA,MAAsB,MAAM,EAAE,CAAC,CAAC,IAAI;;EAG5D,UAAU,IAAI;AACZ,OAAI,OAAO,iBAAkB,QAAO;;EAGtC,KAAK,IAAI;AACP,OAAI,OAAO,oBACT,QAAO;WACJ,QAAQ;kCACe,WAAW;;;;;;;;;;;;;;EAgBzC,gBAAgB,QAAQ;AACtB,UAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;IAC/C,MAAM,YAAY,IAAI,OAAO,IAAI,MAAM,IAAI,CAAC;AAC5C,QAAI,aAAa,sBAAsB,aAAa,oBAClD,QAAO,MAAM;AACf,QAAI;KACF,MAAM,cAAc,MAAM,OAAO,mBAC/B,oBACA,SACD;AACD,SAAI,UAAU,gBAAgB,YAAY;AAC1C,SAAI,IAAI,YAAY;aACb,GAAG;AACV,YAAO,OAAO,OAAO,MACnB,4CAA4C,IAC7C;AACD,SAAI,aAAa;AACjB,SAAI,IAAI,iCAAiC;;KAE3C;;EAEL;;;;;;;;;;;;;AClFH,SAAgB,sBAAgC;AAC9C,QAAO,CAAC,6BAA6B,EAAE,2BAA2B,CAAC;;AAGrE,SAAS,8BAAsC;CAC7C,IAAI;AAEJ,QAAO;EACL,MAAM;EAEN,eAAe,QAAwB;GACrC,MAAM,OAAO,OAAO;AAMpB,gBACE,OANiB;IACjB;IACA;IACA;IACD,CAGa,MAAM,OAAA,GAAA,QAAA,aAAA,GAAA,UAAA,MAAsB,MAAM,EAAE,CAAC,CAAC,IAChD;;EAGN,gBAAgB,QAAQ;AACtB,UAAO,YAAY,IAAI,kBAAkB,OAAO,MAAM,QAAQ;AAC5D,QAAI;KACF,MAAM,eAAe,MAAM,cACzB,QACA,OAAO,OAAO,QACd,WACD;AAED,SAAI,UAAU,gBAAgB,mBAAmB;AACjD,SAAI,UAAU,+BAA+B,IAAI;AACjD,SAAI,IAAI,KAAK,UAAU,aAAa,CAAC;aAC9B,KAAK;AACZ,YAAO,OAAO,OAAO,MACnB,qCAAqC,MACtC;AACD,SAAI,aAAa;AACjB,SAAI,IAAI,KAAK,UAAU,EAAE,OAAO,OAAO,IAAI,EAAE,CAAC,CAAC;;KAEjD;;EAGJ,iBAAiB;AAGf,QAAK,KACH,yIAED;AACD,QAAK,SAAS;IACZ,MAAM;IACN,UAAU;IACV,QAAQ,KAAK,UAAU,EAAE,CAAC;IAC3B,CAAC;;EAEL;;;;;;;;AASH,eAAe,cACb,QACA,QACA,gBACoB;CACpB,IAAI;AACJ,KAAI;AACF,QAAM,MAAM,OAAO,cAAc,kBAAkB,wBAAwB;UACpE,KAAK;EACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAMhE,MAJE,sGAAsG,KACpG,QACD,EAEa;GACd,MAAM,aAAa,kBAAkB;GAErC,MAAM,UADiB,WAAW,SAAS,gBAAgB,GAEvD,gJAEA,iBAAiB,WAAW;AAEhC,WAAQ,KACN,0CAA0C,WAAW,4HAGnD,UACA,mIAEH;QAED,SAAQ,KAAK,yCAAyC,UAAU;AAElE,SAAO,EAAE;;CAGX,MAAM,aAAa,IAAI;AACvB,KAAI,CAAC,YAAY;AACf,MAAI,gBAAgB,SAAS,iBAAiB,CAC5C,SAAQ,KACN,oHAED;AAEH,SAAO,EAAE;;AAGX,KAAI,CAAC,MAAM,QAAQ,WAAW,EAAE;AAC9B,UAAQ,KACN,qDAAqD,OAAO,WAAW,+BACxE;AACD,SAAO,EAAE;;CAGX,MAAM,YAAY;AAGlB,KAAI,OACF,MAAK,MAAM,YAAY,WAAW;EAChC,MAAM,SAAS,iBAAiB,SAAS;AACzC,MAAI,CAAC,OAAO,SAAS;GACnB,MAAM,OAAQ,SAA+B,QAAQ;AACrD,UAAO,KACL,iCAAiC,KAAK,QACpC,OAAO,OAAO,KAAK,MAAM,OAAO,EAAE,KAAK,IAAI,EAAE,UAAU,CAAC,KAAK,KAAK,CACrE;;;AAKP,QAAO,UAAU,KACd,EAAE,WAAW,YAAY,GAAG,WAAoC,KAClE;;;;ACnEH,MAAM,0BAA0B;CAC9B;CACA;CACA;CACA;CACD;;;;;AAMD,SAAgB,gBAAgB,QAAyB;AACvD,QAAO,wBAAwB,MAAM,YAAY,QAAQ,KAAK,OAAO,CAAC;;;;;;;;;;;;;ACtFxE,SAAgB,qBAA6B;AAC3C,QAAO;EACL,MAAM;EAEN,gBAAgB,QAAQ;AAEtB,UAAO,YAAY,IAAI,gBAAgB,OAAO,KAAK,QAAQ;AACzD,QAAI;KAEF,MAAM,YAAA,GAAA,UAAA,SAAmB,OAAO,OAAO,MAAM,eAAe;KAC5D,IAAI;AACJ,SAAI;AACF,aAAO,OAAA,GAAA,iBAAA,UAAe,UAAU,QAAQ;aAClC;AAEN,aAAO,uBAAuB;;AAIhC,YAAO,MAAM,OAAO,mBAAmB,gBAAgB,KAAK;AAE5D,SAAI,UAAU,gBAAgB,YAAY;KAC1C,MAAM,SAAS,IAAI,QAAQ;AAC3B,SAAI,UAAU,gBAAgB,OAAO,EAAE;AACrC,UAAI,UAAU,+BAA+B,OAAO;AACpD,UAAI,UAAU,QAAQ,SAAS;;AAEjC,SAAI,aAAa;AACjB,SAAI,IAAI,KAAK;aACN,KAAK;AACZ,YAAO,OAAO,OAAO,MAAM,oCAAoC,MAAM;AACrE,SAAI,aAAa;AACjB,SAAI,IAAI,yBAAyB;;KAEnC;;EAEL;;AAGH,SAAS,wBAAgC;AACvC,QAAO"}
|
package/dist/vite/index.d.cts
CHANGED
|
@@ -21,5 +21,17 @@ declare function fluidManifestPlugin(): Plugin[];
|
|
|
21
21
|
*/
|
|
22
22
|
declare function fluidBuilderPreviewPlugin(): Plugin;
|
|
23
23
|
//#endregion
|
|
24
|
-
|
|
24
|
+
//#region src/vite/preview-plugin.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* Vite plugin that serves the widget preview route.
|
|
27
|
+
*
|
|
28
|
+
* Dev mode: serves `/__preview__` by transforming preview.html through
|
|
29
|
+
* Vite's HTML pipeline (so import resolution and HMR work).
|
|
30
|
+
*
|
|
31
|
+
* Build mode: no-op — the production preview.html is handled by adding
|
|
32
|
+
* it as a second Rollup input in vite.config.ts.
|
|
33
|
+
*/
|
|
34
|
+
declare function fluidPreviewPlugin(): Plugin;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { fluidBuilderPreviewPlugin, fluidManifestPlugin, fluidPreviewPlugin };
|
|
25
37
|
//# sourceMappingURL=index.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/vite/manifest-plugin.ts","../../src/vite/builder-preview-plugin.ts"],"mappings":";;;;;AAeA;;;;;;;iBAAgB,mBAAA,CAAA,GAAuB,MAAA;;;;;AAAvC;;;;iBCcgB,yBAAA,CAAA,GAA6B,MAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/vite/manifest-plugin.ts","../../src/vite/builder-preview-plugin.ts","../../src/vite/preview-plugin.ts"],"mappings":";;;;;AAeA;;;;;;;iBAAgB,mBAAA,CAAA,GAAuB,MAAA;;;;;AAAvC;;;;iBCcgB,yBAAA,CAAA,GAA6B,MAAA;;;;;ADd7C;;;;;;;iBEDgB,kBAAA,CAAA,GAAsB,MAAA"}
|
package/dist/vite/index.d.mts
CHANGED
|
@@ -21,5 +21,17 @@ declare function fluidManifestPlugin(): Plugin[];
|
|
|
21
21
|
*/
|
|
22
22
|
declare function fluidBuilderPreviewPlugin(): Plugin;
|
|
23
23
|
//#endregion
|
|
24
|
-
|
|
24
|
+
//#region src/vite/preview-plugin.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* Vite plugin that serves the widget preview route.
|
|
27
|
+
*
|
|
28
|
+
* Dev mode: serves `/__preview__` by transforming preview.html through
|
|
29
|
+
* Vite's HTML pipeline (so import resolution and HMR work).
|
|
30
|
+
*
|
|
31
|
+
* Build mode: no-op — the production preview.html is handled by adding
|
|
32
|
+
* it as a second Rollup input in vite.config.ts.
|
|
33
|
+
*/
|
|
34
|
+
declare function fluidPreviewPlugin(): Plugin;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { fluidBuilderPreviewPlugin, fluidManifestPlugin, fluidPreviewPlugin };
|
|
25
37
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/vite/manifest-plugin.ts","../../src/vite/builder-preview-plugin.ts"],"mappings":";;;;;AAeA;;;;;;;iBAAgB,mBAAA,CAAA,GAAuB,MAAA;;;;;AAAvC;;;;iBCcgB,yBAAA,CAAA,GAA6B,MAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/vite/manifest-plugin.ts","../../src/vite/builder-preview-plugin.ts","../../src/vite/preview-plugin.ts"],"mappings":";;;;;AAeA;;;;;;;iBAAgB,mBAAA,CAAA,GAAuB,MAAA;;;;;AAAvC;;;;iBCcgB,yBAAA,CAAA,GAA6B,MAAA;;;;;ADd7C;;;;;;;iBEDgB,kBAAA,CAAA,GAAsB,MAAA"}
|
package/dist/vite/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
3
4
|
//#region src/vite/validate-manifest.ts
|
|
4
5
|
/**
|
|
5
6
|
* Lightweight manifest validation for the SDK vite plugin.
|
|
@@ -254,6 +255,79 @@ async function loadManifests(server, logger, configFilePath) {
|
|
|
254
255
|
return manifests.map(({ component: _component, ...rest }) => rest);
|
|
255
256
|
}
|
|
256
257
|
//#endregion
|
|
257
|
-
|
|
258
|
+
//#region ../core/src/preview/protocol.ts
|
|
259
|
+
const ALLOWED_ORIGIN_PATTERNS = [
|
|
260
|
+
/^https?:\/\/localhost(:\d+)?$/,
|
|
261
|
+
/^https?:\/\/127\.0\.0\.1(:\d+)?$/,
|
|
262
|
+
/^https:\/\/[a-z0-9-]+\.portal\.fluid\.app$/,
|
|
263
|
+
/^https:\/\/[a-z0-9-]+\.fluid\.app$/
|
|
264
|
+
];
|
|
265
|
+
/**
|
|
266
|
+
* Validate that a postMessage origin is trusted.
|
|
267
|
+
* Accepts localhost (any port), *.portal.fluid.app, and *.fluid.app.
|
|
268
|
+
*/
|
|
269
|
+
function isAllowedOrigin(origin) {
|
|
270
|
+
return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/vite/preview-plugin.ts
|
|
274
|
+
/**
|
|
275
|
+
* Vite plugin that serves the widget preview route.
|
|
276
|
+
*
|
|
277
|
+
* Dev mode: serves `/__preview__` by transforming preview.html through
|
|
278
|
+
* Vite's HTML pipeline (so import resolution and HMR work).
|
|
279
|
+
*
|
|
280
|
+
* Build mode: no-op — the production preview.html is handled by adding
|
|
281
|
+
* it as a second Rollup input in vite.config.ts.
|
|
282
|
+
*/
|
|
283
|
+
function fluidPreviewPlugin() {
|
|
284
|
+
return {
|
|
285
|
+
name: "fluid-preview-plugin",
|
|
286
|
+
configureServer(server) {
|
|
287
|
+
server.middlewares.use("/__preview__", async (req, res) => {
|
|
288
|
+
try {
|
|
289
|
+
const htmlPath = resolve(server.config.root, "preview.html");
|
|
290
|
+
let html;
|
|
291
|
+
try {
|
|
292
|
+
html = await readFile(htmlPath, "utf-8");
|
|
293
|
+
} catch {
|
|
294
|
+
html = getDefaultPreviewHtml();
|
|
295
|
+
}
|
|
296
|
+
html = await server.transformIndexHtml("/__preview__", html);
|
|
297
|
+
res.setHeader("Content-Type", "text/html");
|
|
298
|
+
const origin = req.headers.origin;
|
|
299
|
+
if (origin && isAllowedOrigin(origin)) {
|
|
300
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
301
|
+
res.setHeader("Vary", "Origin");
|
|
302
|
+
}
|
|
303
|
+
res.statusCode = 200;
|
|
304
|
+
res.end(html);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
server.config.logger.error(`[fluid] Failed to serve preview: ${err}`);
|
|
307
|
+
res.statusCode = 500;
|
|
308
|
+
res.end("Preview failed to load");
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function getDefaultPreviewHtml() {
|
|
315
|
+
return `<!doctype html>
|
|
316
|
+
<html lang="en">
|
|
317
|
+
<head>
|
|
318
|
+
<meta charset="UTF-8" />
|
|
319
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
320
|
+
<title>Widget Preview</title>
|
|
321
|
+
<meta name="referrer" content="strict-origin-when-cross-origin" />
|
|
322
|
+
<style>body { margin: 0; padding: 0; }</style>
|
|
323
|
+
</head>
|
|
324
|
+
<body>
|
|
325
|
+
<div id="preview-root"></div>
|
|
326
|
+
<script type="module" src="/src/preview-entry.tsx"><\/script>
|
|
327
|
+
</body>
|
|
328
|
+
</html>`;
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
export { fluidBuilderPreviewPlugin, fluidManifestPlugin, fluidPreviewPlugin };
|
|
258
332
|
|
|
259
333
|
//# sourceMappingURL=index.mjs.map
|
package/dist/vite/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/vite/validate-manifest.ts","../../src/vite/builder-preview-plugin.ts","../../src/vite/manifest-plugin.ts"],"sourcesContent":["/**\n * Lightweight manifest validation for the SDK vite plugin.\n *\n * Inlined here (rather than imported from @fluid-app/portal-core) because\n * portal-core is private and not published to npm. This avoids a runtime\n * ERR_MODULE_NOT_FOUND for portals installed from npm.\n */\n\nconst VALID_FIELD_TYPES = [\n \"text\",\n \"textarea\",\n \"number\",\n \"boolean\",\n \"select\",\n \"color\",\n \"range\",\n \"dataSource\",\n \"resource\",\n \"image\",\n \"alignment\",\n \"slider\",\n \"colorPicker\",\n \"sectionHeader\",\n \"separator\",\n \"buttonGroup\",\n \"colorSelect\",\n \"sectionLayoutSelect\",\n \"background\",\n \"contentPosition\",\n \"textSizeSelect\",\n \"cssUnit\",\n \"fontPicker\",\n \"stringArray\",\n \"borderRadius\",\n \"screenPicker\",\n] as const;\n\ninterface ValidationError {\n path: string;\n message: string;\n}\n\ntype ValidationResult =\n | { success: true }\n | { success: false; errors: ValidationError[] };\n\nexport function validateManifest(input: unknown): ValidationResult {\n const errors: ValidationError[] = [];\n const m = input as Record<string, unknown>;\n\n if (!m || typeof m !== \"object\") {\n return {\n success: false,\n errors: [{ path: \"\", message: \"Manifest must be an object\" }],\n };\n }\n\n // Required string fields\n for (const key of [\n \"type\",\n \"displayName\",\n \"description\",\n \"icon\",\n \"category\",\n ]) {\n if (typeof m[key] !== \"string\" || (m[key] as string).length === 0) {\n errors.push({\n path: key,\n message: `${key} is required and must be a non-empty string`,\n });\n }\n }\n\n if (typeof m.manifestVersion !== \"number\" || m.manifestVersion < 1) {\n errors.push({\n path: \"manifestVersion\",\n message: \"manifestVersion must be a positive integer\",\n });\n }\n\n if (typeof m.component !== \"function\") {\n errors.push({\n path: \"component\",\n message: \"component must be a React component (function)\",\n });\n }\n\n // Property schema validation\n const schema = m.propertySchema as Record<string, unknown> | undefined;\n if (schema && typeof schema === \"object\") {\n if (typeof schema.widgetType !== \"string\" || !schema.widgetType) {\n errors.push({\n path: \"propertySchema.widgetType\",\n message: \"widgetType is required\",\n });\n }\n if (typeof m.type === \"string\" && schema.widgetType !== m.type) {\n errors.push({\n path: \"propertySchema.widgetType\",\n message: \"manifest.type must match manifest.propertySchema.widgetType\",\n });\n }\n if (Array.isArray(schema.fields)) {\n for (let i = 0; i < schema.fields.length; i++) {\n const field = schema.fields[i] as Record<string, unknown>;\n if (!field || typeof field.type !== \"string\") continue;\n if (\n !VALID_FIELD_TYPES.includes(\n field.type as (typeof VALID_FIELD_TYPES)[number],\n )\n ) {\n errors.push({\n path: `propertySchema.fields.${i}.type`,\n message: `Invalid field type \"${field.type}\". Valid types: ${VALID_FIELD_TYPES.join(\", \")}`,\n });\n }\n }\n }\n }\n\n return errors.length === 0 ? { success: true } : { success: false, errors };\n}\n","import type { Plugin, ResolvedConfig } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nconst VIRTUAL_ENTRY_ID = \"virtual:builder-preview-entry\";\nconst RESOLVED_VIRTUAL_ID = \"\\0\" + VIRTUAL_ENTRY_ID;\n\nconst RAW_HTML = `<!doctype html>\n<html lang=\"en\" data-theme-mode=\"dark\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Custom Widget Preview</title>\n <style>\n body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }\n </style>\n </head>\n <body>\n <div id=\"builder-preview-root\"></div>\n <script type=\"module\" src=\"/@id/virtual:builder-preview-entry\"></script>\n </body>\n</html>`;\n\n/**\n * Vite plugin that serves a standalone widget preview page at /builder-preview.\n *\n * Dev mode only. Renders all customWidgets from portal.config.ts with\n * a live preview and property editor — no auth, no iframe, no fluid-admin needed.\n */\nexport function fluidBuilderPreviewPlugin(): Plugin {\n let configPath: string;\n let cssPath: string;\n\n return {\n name: \"fluid-builder-preview\",\n apply: \"serve\",\n\n configResolved(config: ResolvedConfig) {\n const root = config.root;\n const candidates = [\"src/portal.config.ts\", \"portal.config.ts\"];\n configPath =\n candidates.find((c) => existsSync(join(root, c))) ??\n \"src/portal.config.ts\";\n const cssCandidates = [\n \"src/index.css\",\n \"src/styles/index.css\",\n \"index.css\",\n ];\n cssPath =\n cssCandidates.find((c) => existsSync(join(root, c))) ?? \"src/index.css\";\n },\n\n resolveId(id) {\n if (id === VIRTUAL_ENTRY_ID) return RESOLVED_VIRTUAL_ID;\n },\n\n load(id) {\n if (id === RESOLVED_VIRTUAL_ID) {\n return `\nimport \"/${cssPath}\";\nimport * as portalConfig from \"/${configPath}\";\nimport { createRoot } from \"react-dom/client\";\nimport { createElement } from \"react\";\nimport { BuilderPreviewApp } from \"@fluid-app/portal-preview\";\n\nconst widgets = portalConfig.customWidgets || [];\nconst root = document.getElementById(\"builder-preview-root\");\nif (root) {\n createRoot(root).render(\n createElement(BuilderPreviewApp, { widgets })\n );\n}\n`;\n }\n },\n\n configureServer(server) {\n server.middlewares.use(async (req, res, next) => {\n const pathname = (req.url ?? \"\").split(\"?\")[0];\n if (pathname !== \"/builder-preview\" && pathname !== \"/builder-preview/\")\n return next();\n try {\n const transformed = await server.transformIndexHtml(\n \"/builder-preview\",\n RAW_HTML,\n );\n res.setHeader(\"Content-Type\", \"text/html\");\n res.end(transformed);\n } catch (e) {\n server.config.logger.error(\n `[fluid] Failed to serve builder preview: ${e}`,\n );\n res.statusCode = 500;\n res.end(\"Builder preview failed to load\");\n }\n });\n },\n };\n}\n","import type { Plugin, ResolvedConfig, ViteDevServer, Logger } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateManifest } from \"./validate-manifest\";\nimport { fluidBuilderPreviewPlugin } from \"./builder-preview-plugin\";\n\n/**\n * Vite plugin bundle that serves widget manifest metadata and the builder preview.\n *\n * Returns an array of plugins:\n * 1. Manifest plugin — serves /__manifests__ (dev) and emits __manifests__.json (build)\n * 2. Builder preview plugin — serves /builder-preview with live widget editing (dev only)\n *\n * Every portal using `fluidManifestPlugin()` automatically gets the builder preview.\n */\nexport function fluidManifestPlugin(): Plugin[] {\n return [fluidManifestPluginInternal(), fluidBuilderPreviewPlugin()];\n}\n\nfunction fluidManifestPluginInternal(): Plugin {\n let configPath: string;\n\n return {\n name: \"fluid-manifest-plugin\",\n\n configResolved(config: ResolvedConfig) {\n const root = config.root;\n const candidates = [\n \"src/widgets.config.ts\",\n \"src/portal.config.ts\",\n \"portal.config.ts\",\n ];\n configPath =\n \"/\" +\n (candidates.find((c) => existsSync(join(root, c))) ??\n \"src/portal.config.ts\");\n },\n\n configureServer(server) {\n server.middlewares.use(\"/__manifests__\", async (_req, res) => {\n try {\n const serializable = await loadManifests(\n server,\n server.config.logger,\n configPath,\n );\n\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.end(JSON.stringify(serializable));\n } catch (err) {\n server.config.logger.error(\n `[fluid] Failed to load manifests: ${err}`,\n );\n res.statusCode = 500;\n res.end(JSON.stringify({ error: String(err) }));\n }\n });\n },\n\n generateBundle() {\n // Build mode: emit placeholder. The CLI extraction utility handles\n // actual build-time manifest extraction via tsx subprocess.\n this.warn(\n \"[fluid] fluidManifestPlugin: emitting empty __manifests__.json. \" +\n \"Run `fluid build` instead of `vite build` to include widget manifests.\",\n );\n this.emitFile({\n type: \"asset\",\n fileName: \"__manifests__.json\",\n source: JSON.stringify([]),\n });\n },\n };\n}\n\n/**\n * Load and serialize manifests from the resolved widget config\n * (widgets.config.ts or portal.config.ts) via Vite's ssrLoadModule.\n * Validates each manifest before stripping the `component` field.\n * Returns an empty array if the config module or export is missing/invalid.\n */\nasync function loadManifests(\n server: ViteDevServer,\n logger?: Logger,\n configFilePath?: string,\n): Promise<unknown[]> {\n let mod: Record<string, unknown>;\n try {\n mod = await server.ssrLoadModule(configFilePath ?? \"/src/portal.config.ts\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const isSSRError =\n /(document|window|navigator|localStorage|sessionStorage|location|history|HTMLElement) is not defined/.test(\n message,\n );\n\n if (isSSRError) {\n const configFile = configFilePath ?? \"/src/portal.config.ts\";\n const isPortalConfig = configFile.includes(\"portal.config\");\n const fixHint = isPortalConfig\n ? ` Fix: Create src/widgets.config.ts with only your customWidgets export.\\n` +\n ` The manifest plugin will load it instead of portal.config.ts.\\n`\n : ` Fix: Ensure ${configFile} does not import browser-only code.\\n`;\n\n logger?.warn(\n `[fluid] Cannot load widget manifests — ${configFile} imports ` +\n `browser-only code that fails during server-side evaluation.\\n` +\n ` Custom widgets will not appear in the builder.\\n` +\n fixHint +\n ` Widget components are fine — the issue is usually screen imports\\n` +\n ` (e.g. DashboardScreen) that pull in the SDK barrel export.`,\n );\n } else {\n logger?.warn(`[fluid] Could not load widget config: ${message}`);\n }\n return [];\n }\n\n const rawWidgets = mod.customWidgets;\n if (!rawWidgets) {\n if (configFilePath?.includes(\"widgets.config\")) {\n logger?.warn(\n `[fluid] widgets.config.ts was loaded but exports no customWidgets. ` +\n `Custom widgets will not appear in the builder.`,\n );\n }\n return [];\n }\n\n if (!Array.isArray(rawWidgets)) {\n logger?.warn(\n `[fluid] customWidgets export is not an array (got ${typeof rawWidgets}). Skipping manifest serving.`,\n );\n return [];\n }\n\n const manifests = rawWidgets as Record<string, unknown>[];\n\n // Validate full manifests (with component) before stripping\n if (logger) {\n for (const manifest of manifests) {\n const result = validateManifest(manifest);\n if (!result.success) {\n const type = (manifest as { type?: string }).type ?? \"unknown\";\n logger.warn(\n `[fluid] Invalid manifest for \"${type}\":\\n` +\n result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\"),\n );\n }\n }\n }\n\n return manifests.map(\n ({ component: _component, ...rest }: Record<string, unknown>) => rest,\n );\n}\n"],"mappings":";;;;;;;;;;AAQA,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAWD,SAAgB,iBAAiB,OAAkC;CACjE,MAAM,SAA4B,EAAE;CACpC,MAAM,IAAI;AAEV,KAAI,CAAC,KAAK,OAAO,MAAM,SACrB,QAAO;EACL,SAAS;EACT,QAAQ,CAAC;GAAE,MAAM;GAAI,SAAS;GAA8B,CAAC;EAC9D;AAIH,MAAK,MAAM,OAAO;EAChB;EACA;EACA;EACA;EACA;EACD,CACC,KAAI,OAAO,EAAE,SAAS,YAAa,EAAE,KAAgB,WAAW,EAC9D,QAAO,KAAK;EACV,MAAM;EACN,SAAS,GAAG,IAAI;EACjB,CAAC;AAIN,KAAI,OAAO,EAAE,oBAAoB,YAAY,EAAE,kBAAkB,EAC/D,QAAO,KAAK;EACV,MAAM;EACN,SAAS;EACV,CAAC;AAGJ,KAAI,OAAO,EAAE,cAAc,WACzB,QAAO,KAAK;EACV,MAAM;EACN,SAAS;EACV,CAAC;CAIJ,MAAM,SAAS,EAAE;AACjB,KAAI,UAAU,OAAO,WAAW,UAAU;AACxC,MAAI,OAAO,OAAO,eAAe,YAAY,CAAC,OAAO,WACnD,QAAO,KAAK;GACV,MAAM;GACN,SAAS;GACV,CAAC;AAEJ,MAAI,OAAO,EAAE,SAAS,YAAY,OAAO,eAAe,EAAE,KACxD,QAAO,KAAK;GACV,MAAM;GACN,SAAS;GACV,CAAC;AAEJ,MAAI,MAAM,QAAQ,OAAO,OAAO,CAC9B,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,OAAO,QAAQ,KAAK;GAC7C,MAAM,QAAQ,OAAO,OAAO;AAC5B,OAAI,CAAC,SAAS,OAAO,MAAM,SAAS,SAAU;AAC9C,OACE,CAAC,kBAAkB,SACjB,MAAM,KACP,CAED,QAAO,KAAK;IACV,MAAM,yBAAyB,EAAE;IACjC,SAAS,uBAAuB,MAAM,KAAK,kBAAkB,kBAAkB,KAAK,KAAK;IAC1F,CAAC;;;AAMV,QAAO,OAAO,WAAW,IAAI,EAAE,SAAS,MAAM,GAAG;EAAE,SAAS;EAAO;EAAQ;;;;ACpH7E,MAAM,mBAAmB;AACzB,MAAM,sBAAsB,OAAO;AAEnC,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;AAsBjB,SAAgB,4BAAoC;CAClD,IAAI;CACJ,IAAI;AAEJ,QAAO;EACL,MAAM;EACN,OAAO;EAEP,eAAe,QAAwB;GACrC,MAAM,OAAO,OAAO;AAEpB,gBADmB,CAAC,wBAAwB,mBAAmB,CAElD,MAAM,MAAM,WAAW,KAAK,MAAM,EAAE,CAAC,CAAC,IACjD;AAMF,aALsB;IACpB;IACA;IACA;IACD,CAEe,MAAM,MAAM,WAAW,KAAK,MAAM,EAAE,CAAC,CAAC,IAAI;;EAG5D,UAAU,IAAI;AACZ,OAAI,OAAO,iBAAkB,QAAO;;EAGtC,KAAK,IAAI;AACP,OAAI,OAAO,oBACT,QAAO;WACJ,QAAQ;kCACe,WAAW;;;;;;;;;;;;;;EAgBzC,gBAAgB,QAAQ;AACtB,UAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;IAC/C,MAAM,YAAY,IAAI,OAAO,IAAI,MAAM,IAAI,CAAC;AAC5C,QAAI,aAAa,sBAAsB,aAAa,oBAClD,QAAO,MAAM;AACf,QAAI;KACF,MAAM,cAAc,MAAM,OAAO,mBAC/B,oBACA,SACD;AACD,SAAI,UAAU,gBAAgB,YAAY;AAC1C,SAAI,IAAI,YAAY;aACb,GAAG;AACV,YAAO,OAAO,OAAO,MACnB,4CAA4C,IAC7C;AACD,SAAI,aAAa;AACjB,SAAI,IAAI,iCAAiC;;KAE3C;;EAEL;;;;;;;;;;;;;AClFH,SAAgB,sBAAgC;AAC9C,QAAO,CAAC,6BAA6B,EAAE,2BAA2B,CAAC;;AAGrE,SAAS,8BAAsC;CAC7C,IAAI;AAEJ,QAAO;EACL,MAAM;EAEN,eAAe,QAAwB;GACrC,MAAM,OAAO,OAAO;AAMpB,gBACE,OANiB;IACjB;IACA;IACA;IACD,CAGa,MAAM,MAAM,WAAW,KAAK,MAAM,EAAE,CAAC,CAAC,IAChD;;EAGN,gBAAgB,QAAQ;AACtB,UAAO,YAAY,IAAI,kBAAkB,OAAO,MAAM,QAAQ;AAC5D,QAAI;KACF,MAAM,eAAe,MAAM,cACzB,QACA,OAAO,OAAO,QACd,WACD;AAED,SAAI,UAAU,gBAAgB,mBAAmB;AACjD,SAAI,UAAU,+BAA+B,IAAI;AACjD,SAAI,IAAI,KAAK,UAAU,aAAa,CAAC;aAC9B,KAAK;AACZ,YAAO,OAAO,OAAO,MACnB,qCAAqC,MACtC;AACD,SAAI,aAAa;AACjB,SAAI,IAAI,KAAK,UAAU,EAAE,OAAO,OAAO,IAAI,EAAE,CAAC,CAAC;;KAEjD;;EAGJ,iBAAiB;AAGf,QAAK,KACH,yIAED;AACD,QAAK,SAAS;IACZ,MAAM;IACN,UAAU;IACV,QAAQ,KAAK,UAAU,EAAE,CAAC;IAC3B,CAAC;;EAEL;;;;;;;;AASH,eAAe,cACb,QACA,QACA,gBACoB;CACpB,IAAI;AACJ,KAAI;AACF,QAAM,MAAM,OAAO,cAAc,kBAAkB,wBAAwB;UACpE,KAAK;EACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAMhE,MAJE,sGAAsG,KACpG,QACD,EAEa;GACd,MAAM,aAAa,kBAAkB;GAErC,MAAM,UADiB,WAAW,SAAS,gBAAgB,GAEvD,gJAEA,iBAAiB,WAAW;AAEhC,WAAQ,KACN,0CAA0C,WAAW,4HAGnD,UACA,mIAEH;QAED,SAAQ,KAAK,yCAAyC,UAAU;AAElE,SAAO,EAAE;;CAGX,MAAM,aAAa,IAAI;AACvB,KAAI,CAAC,YAAY;AACf,MAAI,gBAAgB,SAAS,iBAAiB,CAC5C,SAAQ,KACN,oHAED;AAEH,SAAO,EAAE;;AAGX,KAAI,CAAC,MAAM,QAAQ,WAAW,EAAE;AAC9B,UAAQ,KACN,qDAAqD,OAAO,WAAW,+BACxE;AACD,SAAO,EAAE;;CAGX,MAAM,YAAY;AAGlB,KAAI,OACF,MAAK,MAAM,YAAY,WAAW;EAChC,MAAM,SAAS,iBAAiB,SAAS;AACzC,MAAI,CAAC,OAAO,SAAS;GACnB,MAAM,OAAQ,SAA+B,QAAQ;AACrD,UAAO,KACL,iCAAiC,KAAK,QACpC,OAAO,OAAO,KAAK,MAAM,OAAO,EAAE,KAAK,IAAI,EAAE,UAAU,CAAC,KAAK,KAAK,CACrE;;;AAKP,QAAO,UAAU,KACd,EAAE,WAAW,YAAY,GAAG,WAAoC,KAClE"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/vite/validate-manifest.ts","../../src/vite/builder-preview-plugin.ts","../../src/vite/manifest-plugin.ts","../../../core/src/preview/protocol.ts","../../src/vite/preview-plugin.ts"],"sourcesContent":["/**\n * Lightweight manifest validation for the SDK vite plugin.\n *\n * Inlined here (rather than imported from @fluid-app/portal-core) because\n * portal-core is private and not published to npm. This avoids a runtime\n * ERR_MODULE_NOT_FOUND for portals installed from npm.\n */\n\nconst VALID_FIELD_TYPES = [\n \"text\",\n \"textarea\",\n \"number\",\n \"boolean\",\n \"select\",\n \"color\",\n \"range\",\n \"dataSource\",\n \"resource\",\n \"image\",\n \"alignment\",\n \"slider\",\n \"colorPicker\",\n \"sectionHeader\",\n \"separator\",\n \"buttonGroup\",\n \"colorSelect\",\n \"sectionLayoutSelect\",\n \"background\",\n \"contentPosition\",\n \"textSizeSelect\",\n \"cssUnit\",\n \"fontPicker\",\n \"stringArray\",\n \"borderRadius\",\n \"screenPicker\",\n] as const;\n\ninterface ValidationError {\n path: string;\n message: string;\n}\n\ntype ValidationResult =\n | { success: true }\n | { success: false; errors: ValidationError[] };\n\nexport function validateManifest(input: unknown): ValidationResult {\n const errors: ValidationError[] = [];\n const m = input as Record<string, unknown>;\n\n if (!m || typeof m !== \"object\") {\n return {\n success: false,\n errors: [{ path: \"\", message: \"Manifest must be an object\" }],\n };\n }\n\n // Required string fields\n for (const key of [\n \"type\",\n \"displayName\",\n \"description\",\n \"icon\",\n \"category\",\n ]) {\n if (typeof m[key] !== \"string\" || (m[key] as string).length === 0) {\n errors.push({\n path: key,\n message: `${key} is required and must be a non-empty string`,\n });\n }\n }\n\n if (typeof m.manifestVersion !== \"number\" || m.manifestVersion < 1) {\n errors.push({\n path: \"manifestVersion\",\n message: \"manifestVersion must be a positive integer\",\n });\n }\n\n if (typeof m.component !== \"function\") {\n errors.push({\n path: \"component\",\n message: \"component must be a React component (function)\",\n });\n }\n\n // Property schema validation\n const schema = m.propertySchema as Record<string, unknown> | undefined;\n if (schema && typeof schema === \"object\") {\n if (typeof schema.widgetType !== \"string\" || !schema.widgetType) {\n errors.push({\n path: \"propertySchema.widgetType\",\n message: \"widgetType is required\",\n });\n }\n if (typeof m.type === \"string\" && schema.widgetType !== m.type) {\n errors.push({\n path: \"propertySchema.widgetType\",\n message: \"manifest.type must match manifest.propertySchema.widgetType\",\n });\n }\n if (Array.isArray(schema.fields)) {\n for (let i = 0; i < schema.fields.length; i++) {\n const field = schema.fields[i] as Record<string, unknown>;\n if (!field || typeof field.type !== \"string\") continue;\n if (\n !VALID_FIELD_TYPES.includes(\n field.type as (typeof VALID_FIELD_TYPES)[number],\n )\n ) {\n errors.push({\n path: `propertySchema.fields.${i}.type`,\n message: `Invalid field type \"${field.type}\". Valid types: ${VALID_FIELD_TYPES.join(\", \")}`,\n });\n }\n }\n }\n }\n\n return errors.length === 0 ? { success: true } : { success: false, errors };\n}\n","import type { Plugin, ResolvedConfig } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nconst VIRTUAL_ENTRY_ID = \"virtual:builder-preview-entry\";\nconst RESOLVED_VIRTUAL_ID = \"\\0\" + VIRTUAL_ENTRY_ID;\n\nconst RAW_HTML = `<!doctype html>\n<html lang=\"en\" data-theme-mode=\"dark\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Custom Widget Preview</title>\n <style>\n body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }\n </style>\n </head>\n <body>\n <div id=\"builder-preview-root\"></div>\n <script type=\"module\" src=\"/@id/virtual:builder-preview-entry\"></script>\n </body>\n</html>`;\n\n/**\n * Vite plugin that serves a standalone widget preview page at /builder-preview.\n *\n * Dev mode only. Renders all customWidgets from portal.config.ts with\n * a live preview and property editor — no auth, no iframe, no fluid-admin needed.\n */\nexport function fluidBuilderPreviewPlugin(): Plugin {\n let configPath: string;\n let cssPath: string;\n\n return {\n name: \"fluid-builder-preview\",\n apply: \"serve\",\n\n configResolved(config: ResolvedConfig) {\n const root = config.root;\n const candidates = [\"src/portal.config.ts\", \"portal.config.ts\"];\n configPath =\n candidates.find((c) => existsSync(join(root, c))) ??\n \"src/portal.config.ts\";\n const cssCandidates = [\n \"src/index.css\",\n \"src/styles/index.css\",\n \"index.css\",\n ];\n cssPath =\n cssCandidates.find((c) => existsSync(join(root, c))) ?? \"src/index.css\";\n },\n\n resolveId(id) {\n if (id === VIRTUAL_ENTRY_ID) return RESOLVED_VIRTUAL_ID;\n },\n\n load(id) {\n if (id === RESOLVED_VIRTUAL_ID) {\n return `\nimport \"/${cssPath}\";\nimport * as portalConfig from \"/${configPath}\";\nimport { createRoot } from \"react-dom/client\";\nimport { createElement } from \"react\";\nimport { BuilderPreviewApp } from \"@fluid-app/portal-preview\";\n\nconst widgets = portalConfig.customWidgets || [];\nconst root = document.getElementById(\"builder-preview-root\");\nif (root) {\n createRoot(root).render(\n createElement(BuilderPreviewApp, { widgets })\n );\n}\n`;\n }\n },\n\n configureServer(server) {\n server.middlewares.use(async (req, res, next) => {\n const pathname = (req.url ?? \"\").split(\"?\")[0];\n if (pathname !== \"/builder-preview\" && pathname !== \"/builder-preview/\")\n return next();\n try {\n const transformed = await server.transformIndexHtml(\n \"/builder-preview\",\n RAW_HTML,\n );\n res.setHeader(\"Content-Type\", \"text/html\");\n res.end(transformed);\n } catch (e) {\n server.config.logger.error(\n `[fluid] Failed to serve builder preview: ${e}`,\n );\n res.statusCode = 500;\n res.end(\"Builder preview failed to load\");\n }\n });\n },\n };\n}\n","import type { Plugin, ResolvedConfig, ViteDevServer, Logger } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateManifest } from \"./validate-manifest\";\nimport { fluidBuilderPreviewPlugin } from \"./builder-preview-plugin\";\n\n/**\n * Vite plugin bundle that serves widget manifest metadata and the builder preview.\n *\n * Returns an array of plugins:\n * 1. Manifest plugin — serves /__manifests__ (dev) and emits __manifests__.json (build)\n * 2. Builder preview plugin — serves /builder-preview with live widget editing (dev only)\n *\n * Every portal using `fluidManifestPlugin()` automatically gets the builder preview.\n */\nexport function fluidManifestPlugin(): Plugin[] {\n return [fluidManifestPluginInternal(), fluidBuilderPreviewPlugin()];\n}\n\nfunction fluidManifestPluginInternal(): Plugin {\n let configPath: string;\n\n return {\n name: \"fluid-manifest-plugin\",\n\n configResolved(config: ResolvedConfig) {\n const root = config.root;\n const candidates = [\n \"src/widgets.config.ts\",\n \"src/portal.config.ts\",\n \"portal.config.ts\",\n ];\n configPath =\n \"/\" +\n (candidates.find((c) => existsSync(join(root, c))) ??\n \"src/portal.config.ts\");\n },\n\n configureServer(server) {\n server.middlewares.use(\"/__manifests__\", async (_req, res) => {\n try {\n const serializable = await loadManifests(\n server,\n server.config.logger,\n configPath,\n );\n\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.end(JSON.stringify(serializable));\n } catch (err) {\n server.config.logger.error(\n `[fluid] Failed to load manifests: ${err}`,\n );\n res.statusCode = 500;\n res.end(JSON.stringify({ error: String(err) }));\n }\n });\n },\n\n generateBundle() {\n // Build mode: emit placeholder. The CLI extraction utility handles\n // actual build-time manifest extraction via tsx subprocess.\n this.warn(\n \"[fluid] fluidManifestPlugin: emitting empty __manifests__.json. \" +\n \"Run `fluid build` instead of `vite build` to include widget manifests.\",\n );\n this.emitFile({\n type: \"asset\",\n fileName: \"__manifests__.json\",\n source: JSON.stringify([]),\n });\n },\n };\n}\n\n/**\n * Load and serialize manifests from the resolved widget config\n * (widgets.config.ts or portal.config.ts) via Vite's ssrLoadModule.\n * Validates each manifest before stripping the `component` field.\n * Returns an empty array if the config module or export is missing/invalid.\n */\nasync function loadManifests(\n server: ViteDevServer,\n logger?: Logger,\n configFilePath?: string,\n): Promise<unknown[]> {\n let mod: Record<string, unknown>;\n try {\n mod = await server.ssrLoadModule(configFilePath ?? \"/src/portal.config.ts\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const isSSRError =\n /(document|window|navigator|localStorage|sessionStorage|location|history|HTMLElement) is not defined/.test(\n message,\n );\n\n if (isSSRError) {\n const configFile = configFilePath ?? \"/src/portal.config.ts\";\n const isPortalConfig = configFile.includes(\"portal.config\");\n const fixHint = isPortalConfig\n ? ` Fix: Create src/widgets.config.ts with only your customWidgets export.\\n` +\n ` The manifest plugin will load it instead of portal.config.ts.\\n`\n : ` Fix: Ensure ${configFile} does not import browser-only code.\\n`;\n\n logger?.warn(\n `[fluid] Cannot load widget manifests — ${configFile} imports ` +\n `browser-only code that fails during server-side evaluation.\\n` +\n ` Custom widgets will not appear in the builder.\\n` +\n fixHint +\n ` Widget components are fine — the issue is usually screen imports\\n` +\n ` (e.g. DashboardScreen) that pull in the SDK barrel export.`,\n );\n } else {\n logger?.warn(`[fluid] Could not load widget config: ${message}`);\n }\n return [];\n }\n\n const rawWidgets = mod.customWidgets;\n if (!rawWidgets) {\n if (configFilePath?.includes(\"widgets.config\")) {\n logger?.warn(\n `[fluid] widgets.config.ts was loaded but exports no customWidgets. ` +\n `Custom widgets will not appear in the builder.`,\n );\n }\n return [];\n }\n\n if (!Array.isArray(rawWidgets)) {\n logger?.warn(\n `[fluid] customWidgets export is not an array (got ${typeof rawWidgets}). Skipping manifest serving.`,\n );\n return [];\n }\n\n const manifests = rawWidgets as Record<string, unknown>[];\n\n // Validate full manifests (with component) before stripping\n if (logger) {\n for (const manifest of manifests) {\n const result = validateManifest(manifest);\n if (!result.success) {\n const type = (manifest as { type?: string }).type ?? \"unknown\";\n logger.warn(\n `[fluid] Invalid manifest for \"${type}\":\\n` +\n result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\"),\n );\n }\n }\n }\n\n return manifests.map(\n ({ component: _component, ...rest }: Record<string, unknown>) => rest,\n );\n}\n","/**\n * PostMessage protocol for builder ↔ iframe widget preview communication.\n *\n * Four-step handshake:\n * 1. Iframe sends `ready` once mounted\n * 2. Builder sends `auth` with token (NEVER via URL)\n * 3. Iframe sends `auth-ready` once FluidProvider settles\n * 4. Builder sends `render-widget` with type + props\n *\n * This prevents the race condition where render-widget arrives\n * before the provider stack has initialized with the auth token.\n */\n\n// ---------------------------------------------------------------------------\n// Builder → Iframe commands\n// ---------------------------------------------------------------------------\n\nexport interface AuthCommand {\n type: \"auth\";\n token: string;\n}\n\nexport interface RenderWidgetCommand {\n type: \"render-widget\";\n widgetType: string;\n props: Record<string, unknown>;\n}\n\nexport interface UnmountWidgetCommand {\n type: \"unmount-widget\";\n}\n\nexport interface SelectWidgetCommand {\n type: \"select-widget\";\n widgetType: string;\n}\n\nexport type PreviewCommand =\n | AuthCommand\n | RenderWidgetCommand\n | UnmountWidgetCommand\n | SelectWidgetCommand;\n\n// ---------------------------------------------------------------------------\n// Iframe → Builder events\n// ---------------------------------------------------------------------------\n\nexport interface ReadyEvent {\n type: \"ready\";\n}\n\nexport interface AuthReadyEvent {\n type: \"auth-ready\";\n}\n\nexport interface WidgetRenderedEvent {\n type: \"widget-rendered\";\n widgetType: string;\n dimensions: { width: number; height: number };\n}\n\nexport interface WidgetErrorEvent {\n type: \"widget-error\";\n widgetType: string;\n error: string;\n}\n\nexport interface WidgetClickedEvent {\n type: \"widget-clicked\";\n widgetType: string;\n}\n\nexport interface WidgetHmrEvent {\n type: \"widget-hmr\";\n}\n\nexport type PreviewEvent =\n | ReadyEvent\n | AuthReadyEvent\n | WidgetRenderedEvent\n | WidgetErrorEvent\n | WidgetClickedEvent\n | WidgetHmrEvent;\n\n// ---------------------------------------------------------------------------\n// Origin validation\n// ---------------------------------------------------------------------------\n\nconst ALLOWED_ORIGIN_PATTERNS = [\n /^https?:\\/\\/localhost(:\\d+)?$/,\n /^https?:\\/\\/127\\.0\\.0\\.1(:\\d+)?$/,\n /^https:\\/\\/[a-z0-9-]+\\.portal\\.fluid\\.app$/,\n /^https:\\/\\/[a-z0-9-]+\\.fluid\\.app$/,\n];\n\n/**\n * Validate that a postMessage origin is trusted.\n * Accepts localhost (any port), *.portal.fluid.app, and *.fluid.app.\n */\nexport function isAllowedOrigin(origin: string): boolean {\n return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));\n}\n\n// ---------------------------------------------------------------------------\n// Type guards\n// ---------------------------------------------------------------------------\n\n/** Check if a message is a valid PreviewCommand (builder → iframe). */\nexport function isPreviewCommand(data: unknown): data is PreviewCommand {\n if (typeof data !== \"object\" || data === null) return false;\n const msg = data as Record<string, unknown>;\n switch (msg[\"type\"]) {\n case \"auth\":\n return typeof msg[\"token\"] === \"string\";\n case \"render-widget\":\n return (\n typeof msg[\"widgetType\"] === \"string\" &&\n typeof msg[\"props\"] === \"object\" &&\n msg[\"props\"] !== null &&\n !Array.isArray(msg[\"props\"])\n );\n case \"unmount-widget\":\n return true;\n case \"select-widget\":\n return typeof msg[\"widgetType\"] === \"string\";\n default:\n return false;\n }\n}\n\n/** Check if a message is a valid PreviewEvent (iframe → builder). */\nexport function isPreviewEvent(data: unknown): data is PreviewEvent {\n if (typeof data !== \"object\" || data === null) return false;\n const msg = data as Record<string, unknown>;\n switch (msg[\"type\"]) {\n case \"ready\":\n case \"auth-ready\":\n case \"widget-hmr\":\n return true;\n case \"widget-rendered\":\n return (\n typeof msg[\"widgetType\"] === \"string\" &&\n typeof msg[\"dimensions\"] === \"object\" &&\n msg[\"dimensions\"] !== null &&\n Number.isFinite(\n (msg[\"dimensions\"] as Record<string, unknown>)[\"width\"],\n ) &&\n Number.isFinite(\n (msg[\"dimensions\"] as Record<string, unknown>)[\"height\"],\n )\n );\n case \"widget-error\":\n return (\n typeof msg[\"widgetType\"] === \"string\" &&\n typeof msg[\"error\"] === \"string\"\n );\n case \"widget-clicked\":\n return typeof msg[\"widgetType\"] === \"string\";\n default:\n return false;\n }\n}\n","import { resolve } from \"node:path\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Plugin } from \"vite\";\nimport { isAllowedOrigin } from \"@fluid-app/portal-core/preview/protocol\";\n\n/**\n * Vite plugin that serves the widget preview route.\n *\n * Dev mode: serves `/__preview__` by transforming preview.html through\n * Vite's HTML pipeline (so import resolution and HMR work).\n *\n * Build mode: no-op — the production preview.html is handled by adding\n * it as a second Rollup input in vite.config.ts.\n */\nexport function fluidPreviewPlugin(): Plugin {\n return {\n name: \"fluid-preview-plugin\",\n\n configureServer(server) {\n // Serve the preview HTML shell at /__preview__\n server.middlewares.use(\"/__preview__\", async (req, res) => {\n try {\n // Read the preview.html from the project root (copied from template)\n const htmlPath = resolve(server.config.root, \"preview.html\");\n let html: string;\n try {\n html = await readFile(htmlPath, \"utf-8\");\n } catch {\n // Fallback: serve a minimal preview shell\n html = getDefaultPreviewHtml();\n }\n\n // Transform through Vite's HTML pipeline for HMR + import resolution\n html = await server.transformIndexHtml(\"/__preview__\", html);\n\n res.setHeader(\"Content-Type\", \"text/html\");\n const origin = req.headers.origin;\n if (origin && isAllowedOrigin(origin)) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n res.setHeader(\"Vary\", \"Origin\");\n }\n res.statusCode = 200;\n res.end(html);\n } catch (err) {\n server.config.logger.error(`[fluid] Failed to serve preview: ${err}`);\n res.statusCode = 500;\n res.end(\"Preview failed to load\");\n }\n });\n },\n };\n}\n\nfunction getDefaultPreviewHtml(): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Widget Preview</title>\n <meta name=\"referrer\" content=\"strict-origin-when-cross-origin\" />\n <style>body { margin: 0; padding: 0; }</style>\n </head>\n <body>\n <div id=\"preview-root\"></div>\n <script type=\"module\" src=\"/src/preview-entry.tsx\"></script>\n </body>\n</html>`;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/portal-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.127",
|
|
4
4
|
"description": "SDK for building custom Fluid portals",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
"types": "./dist/vite/index.d.mts",
|
|
30
30
|
"default": "./dist/vite/index.mjs"
|
|
31
31
|
}
|
|
32
|
+
},
|
|
33
|
+
"./preview": {
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/preview/preview-entry.d.mts",
|
|
36
|
+
"default": "./dist/preview/preview-entry.mjs"
|
|
37
|
+
}
|
|
32
38
|
}
|
|
33
39
|
},
|
|
34
40
|
"publishConfig": {
|
|
@@ -66,39 +72,39 @@
|
|
|
66
72
|
"typescript": "^5",
|
|
67
73
|
"zod": "4.3.5",
|
|
68
74
|
"@fluid-app/api-client-core": "0.1.0",
|
|
69
|
-
"@fluid-app/auth": "0.1.0",
|
|
70
75
|
"@fluid-app/cart-ui": "0.1.13",
|
|
71
|
-
"@fluid-app/
|
|
76
|
+
"@fluid-app/auth": "0.1.0",
|
|
72
77
|
"@fluid-app/company-switcher-ui": "0.1.0",
|
|
78
|
+
"@fluid-app/company-switcher-core": "0.1.0",
|
|
73
79
|
"@fluid-app/contacts-api-client": "0.1.0",
|
|
74
|
-
"@fluid-app/contacts-ui": "0.1.0",
|
|
75
|
-
"@fluid-app/fluid-pay-api-client": "0.1.0",
|
|
76
|
-
"@fluid-app/contacts-core": "0.1.0",
|
|
77
80
|
"@fluid-app/file-picker-api-client": "0.1.0",
|
|
81
|
+
"@fluid-app/contacts-core": "0.1.0",
|
|
82
|
+
"@fluid-app/fluid-pay-api-client": "0.1.0",
|
|
78
83
|
"@fluid-app/fluid-pay-core": "0.1.0",
|
|
84
|
+
"@fluid-app/contacts-ui": "0.1.0",
|
|
79
85
|
"@fluid-app/fluidos-api-client": "0.1.0",
|
|
80
86
|
"@fluid-app/messaging-api-client": "0.1.0",
|
|
81
87
|
"@fluid-app/messaging-core": "0.1.0",
|
|
82
88
|
"@fluid-app/messaging-ui": "0.1.0",
|
|
83
|
-
"@fluid-app/mysite-ui": "0.1.0",
|
|
84
89
|
"@fluid-app/orders-api-client": "0.1.0",
|
|
85
|
-
"@fluid-app/
|
|
86
|
-
"@fluid-app/orders-ui": "0.1.0",
|
|
90
|
+
"@fluid-app/mysite-ui": "0.1.0",
|
|
87
91
|
"@fluid-app/permissions": "0.1.0",
|
|
92
|
+
"@fluid-app/orders-ui": "0.1.0",
|
|
93
|
+
"@fluid-app/orders-core": "0.1.0",
|
|
88
94
|
"@fluid-app/portal-app-download-ui": "0.1.0",
|
|
95
|
+
"@fluid-app/portal-core": "0.1.23",
|
|
89
96
|
"@fluid-app/portal-preview": "0.1.0",
|
|
90
97
|
"@fluid-app/portal-pro-upgrade-ui": "0.1.0",
|
|
91
98
|
"@fluid-app/portal-react": "0.1.0",
|
|
92
|
-
"@fluid-app/portal-
|
|
99
|
+
"@fluid-app/portal-tenant-contacts-api-client": "0.1.0",
|
|
93
100
|
"@fluid-app/portal-tenant-api-client": "0.1.0",
|
|
94
101
|
"@fluid-app/portal-tenant-mysite-api-client": "0.1.0",
|
|
95
|
-
"@fluid-app/portal-tenant-contacts-api-client": "0.1.0",
|
|
96
102
|
"@fluid-app/portal-widgets": "0.1.22",
|
|
97
103
|
"@fluid-app/products-core": "0.1.0",
|
|
98
104
|
"@fluid-app/products-api-client": "0.1.0",
|
|
99
105
|
"@fluid-app/profile-core": "0.1.0",
|
|
100
|
-
"@fluid-app/profile-ui": "0.1.0",
|
|
101
106
|
"@fluid-app/query-persister": "0.1.0",
|
|
107
|
+
"@fluid-app/profile-ui": "0.1.0",
|
|
102
108
|
"@fluid-app/shareables-api-client": "0.1.0",
|
|
103
109
|
"@fluid-app/shareables-core": "0.1.0",
|
|
104
110
|
"@fluid-app/shareables-ui": "0.1.0",
|
|
@@ -107,8 +113,8 @@
|
|
|
107
113
|
"@fluid-app/subscriptions-api-client": "0.1.0",
|
|
108
114
|
"@fluid-app/subscriptions-core": "0.1.0",
|
|
109
115
|
"@fluid-app/subscriptions-ui": "0.1.0",
|
|
110
|
-
"@fluid-app/
|
|
111
|
-
"@fluid-app/
|
|
116
|
+
"@fluid-app/typescript-config": "0.0.0",
|
|
117
|
+
"@fluid-app/ui-primitives": "0.1.13"
|
|
112
118
|
},
|
|
113
119
|
"peerDependencies": {
|
|
114
120
|
"@hookform/resolvers": "^5.2.2",
|