@fluid-app/fluid-cli-portal 0.1.16 → 0.1.18

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.
@@ -292,6 +292,62 @@ if (root) {
292
292
  };
293
293
  }
294
294
  //#endregion
295
- export { buildManifestFromLocalFiles, builderPreviewPlugin, portalDevPlugin };
295
+ //#region src/vite-plugin/backend-dev-plugin.ts
296
+ /**
297
+ * Serves dev-compatible responses at production asset paths so the Rails
298
+ * backend can point `PORTAL_CDN_BASE` at `http://localhost:5173` and get
299
+ * HMR + live source without any template changes.
300
+ *
301
+ * Handles:
302
+ * - `/portal.js` → ESM shim that loads `@vite/client` + the real entry
303
+ * - `/portal.css` → empty (Vite injects CSS via JS in dev)
304
+ * - `/vendor.js`, `/query.js` → empty ES modules (no chunks in dev)
305
+ */
306
+ function backendDevPlugin(options) {
307
+ const entry = options?.entry ?? "/src/main.tsx";
308
+ const chunks = options?.chunks ?? ["vendor.js", "query.js"];
309
+ return {
310
+ name: "fluid-backend-dev",
311
+ apply: "serve",
312
+ configureServer(server) {
313
+ server.middlewares.use((req, res, next) => {
314
+ const raw = req.url ?? "";
315
+ const qIndex = raw.indexOf("?");
316
+ const url = qIndex === -1 ? raw : raw.slice(0, qIndex);
317
+ if (url === "/portal.js") {
318
+ serve(res, "application/javascript", [
319
+ `import RefreshRuntime from "/@react-refresh";`,
320
+ `import "/@vite/client";`,
321
+ `RefreshRuntime.injectIntoGlobalHook(window);`,
322
+ `window.$RefreshReg$ = () => {};`,
323
+ `window.$RefreshSig$ = () => (type) => type;`,
324
+ `window.__vite_plugin_react_preamble_installed__ = true;`,
325
+ `await import("${entry}");`
326
+ ].join("\n"));
327
+ return;
328
+ }
329
+ if (url === "/portal.css") {
330
+ serve(res, "text/css", "/* dev: CSS injected via JS */");
331
+ return;
332
+ }
333
+ const chunkName = url.startsWith("/") ? url.slice(1) : url;
334
+ if (chunks.includes(chunkName)) {
335
+ serve(res, "application/javascript", "export {};");
336
+ return;
337
+ }
338
+ next();
339
+ });
340
+ }
341
+ };
342
+ }
343
+ function serve(res, contentType, body) {
344
+ res.setHeader("Content-Type", `${contentType}; charset=utf-8`);
345
+ res.setHeader("Cache-Control", "no-store");
346
+ res.setHeader("Access-Control-Allow-Origin", "*");
347
+ res.statusCode = 200;
348
+ res.end(body);
349
+ }
350
+ //#endregion
351
+ export { backendDevPlugin, buildManifestFromLocalFiles, builderPreviewPlugin, portalDevPlugin };
296
352
 
297
353
  //# sourceMappingURL=vite-plugin.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.mjs","names":[],"sources":["../src/vite-plugin/build-manifest.ts","../src/vite-plugin/portal-dev-plugin.ts","../src/vite-plugin/builder-preview-plugin.ts"],"sourcesContent":["/**\n * Build a RawManifestResponse from local portal/ files.\n *\n * Reads the portal directory structure produced by `fluid portal pull` and\n * assembles it into the same shape the SDK expects from the\n * `/api/fluid_os/definitions/active` endpoint, so the existing\n * `transformManifestToRepAppData()` pipeline works unchanged.\n */\n\nimport { readFile, readdir } from \"node:fs/promises\";\nimport { join, basename, extname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { createHash } from \"node:crypto\";\n\nimport type {\n LocalScreen,\n LocalTheme,\n LocalNavigation,\n LocalNavigationItem,\n LocalProfile,\n} from \"../utils/transform.js\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types matching the RawManifestResponse shape from\n// packages/portal/sdk/src/transforms/index.ts\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface RawNavigationItem {\n id: number;\n label: string;\n slug?: string;\n icon?: string;\n screen_id?: number;\n parent_id?: number;\n source?: string;\n position: number;\n children: RawNavigationItem[];\n}\n\ninterface RawNavigation {\n id: number;\n name?: string;\n definition_id: number;\n navigation_items?: RawNavigationItem[];\n}\n\ninterface RawTheme {\n id: number;\n config?: Record<string, unknown> | null;\n active?: boolean | null;\n name?: string | null;\n}\n\ninterface RawScreen {\n id: number;\n definition_id?: number;\n name?: string | null;\n slug?: string | null;\n component_tree?: unknown;\n}\n\ninterface RawManifestProfile {\n name?: string;\n definition_id: number;\n themes?: RawTheme[];\n navigation?: RawNavigation;\n mobile_navigation?: RawNavigation;\n}\n\ninterface RawManifestResponse {\n manifest: {\n definition_id: number;\n published_version?: number;\n screens?: RawScreen[];\n profile?: RawManifestProfile;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Synthetic ID generation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Generate a deterministic synthetic numeric ID from a string key.\n * Uses a CRC32-like hash so the same slug always produces the same ID\n * within a dev session, while avoiding collisions in practice.\n */\nfunction syntheticId(key: string): number {\n const hash = createHash(\"md5\").update(key).digest();\n // Read first 4 bytes as unsigned 32-bit integer, ensure positive\n return hash.readUInt32BE(0);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// File reading helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Read and parse a JSON file, returning null on failure.\n */\nasync function readJsonFile<T>(filePath: string): Promise<T | null> {\n try {\n const content = await readFile(filePath, \"utf-8\");\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Read all JSON files in a directory, returning an array of [slug, data] tuples.\n * Slug is the filename without extension.\n */\nasync function readJsonDir<T>(\n dirPath: string,\n): Promise<Array<[slug: string, data: T]>> {\n if (!existsSync(dirPath)) return [];\n\n const entries = await readdir(dirPath, { withFileTypes: true });\n const results: Array<[string, T]> = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || extname(entry.name) !== \".json\") continue;\n\n const slug = basename(entry.name, \".json\");\n const data = await readJsonFile<T>(join(dirPath, entry.name));\n if (data != null) {\n results.push([slug, data]);\n }\n }\n\n return results;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Navigation item conversion\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Convert local navigation items (which reference screens by slug)\n * back to the raw API format (which uses screen_id).\n */\nfunction toRawNavigationItems(\n items: LocalNavigationItem[],\n screenSlugToId: Map<string, number>,\n): RawNavigationItem[] {\n return items.map((item) => {\n const rawItem: RawNavigationItem = {\n id: item.id,\n label: item.label ?? \"Untitled\",\n position: item.position ?? 0,\n children: toRawNavigationItems(item.children ?? [], screenSlugToId),\n };\n\n if (item.slug != null) rawItem.slug = item.slug;\n if (item.icon != null) rawItem.icon = item.icon;\n if (item.source != null) rawItem.source = item.source;\n if (item.parent_id != null) rawItem.parent_id = item.parent_id;\n\n // Resolve screen slug -> synthetic screen ID\n if (item.screen != null) {\n const screenId = screenSlugToId.get(item.screen);\n if (screenId != null) {\n rawItem.screen_id = screenId;\n }\n }\n\n return rawItem;\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Main builder\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Build a `RawManifestResponse` from local portal/ files.\n *\n * The returned object matches the shape from `/api/fluid_os/definitions/active`\n * so the SDK's `transformManifestToRepAppData()` works without modification.\n */\nexport async function buildManifestFromLocalFiles(\n portalDir: string,\n): Promise<RawManifestResponse> {\n // 1. Read definition.json\n const definition = await readJsonFile<{ name?: string }>(\n join(portalDir, \"definition.json\"),\n );\n const definitionName = definition?.name ?? \"Local Dev\";\n const definitionId = syntheticId(`definition:${definitionName}`);\n\n // 2. Read all resource files in parallel\n const [screenEntries, themeEntries, navEntries, profileEntries] =\n await Promise.all([\n readJsonDir<LocalScreen>(join(portalDir, \"screens\")),\n readJsonDir<LocalTheme>(join(portalDir, \"themes\")),\n readJsonDir<LocalNavigation>(join(portalDir, \"navigations\")),\n readJsonDir<LocalProfile>(join(portalDir, \"profiles\")),\n ]);\n\n // 3. Build screen slug -> synthetic ID map\n const screenSlugToId = new Map<string, number>();\n for (const [slug] of screenEntries) {\n screenSlugToId.set(slug, syntheticId(`screen:${slug}`));\n }\n\n // 4. Build raw screens\n const rawScreens: RawScreen[] = screenEntries.map(([slug, screen]) => ({\n id: screenSlugToId.get(slug)!,\n definition_id: definitionId,\n name: screen.name,\n slug,\n component_tree: screen.component_tree,\n }));\n\n // 5. Build raw themes (keyed by slug for profile lookup)\n const themeSlugToRaw = new Map<string, RawTheme>();\n for (const [slug, theme] of themeEntries) {\n themeSlugToRaw.set(slug, {\n id: syntheticId(`theme:${slug}`),\n name: theme.name,\n config: theme.config,\n active: theme.active,\n });\n }\n const rawThemes = Array.from(themeSlugToRaw.values());\n\n // 6. Build navigation slug -> synthetic ID map\n const navSlugToId = new Map<string, number>();\n for (const [slug] of navEntries) {\n navSlugToId.set(slug, syntheticId(`navigation:${slug}`));\n }\n\n // Build raw navigations (keyed by slug for profile lookup)\n const rawNavigations = new Map<string, RawNavigation>();\n for (const [slug, nav] of navEntries) {\n rawNavigations.set(slug, {\n id: navSlugToId.get(slug)!,\n name: nav.name,\n definition_id: definitionId,\n navigation_items: toRawNavigationItems(\n nav.navigation_items ?? [],\n screenSlugToId,\n ),\n });\n }\n\n // 7. Find the default profile (or use the first one)\n const defaultProfile =\n profileEntries.find(([, p]) => p.default)?.[1] ??\n profileEntries[0]?.[1] ??\n null;\n\n // 8. Assemble the profile for the manifest\n let rawProfile: RawManifestProfile | undefined;\n if (defaultProfile) {\n // Resolve navigation slugs to raw navigation objects\n const navigation = defaultProfile.navigation\n ? rawNavigations.get(defaultProfile.navigation)\n : undefined;\n\n const mobileNavigation = defaultProfile.mobile_navigation\n ? rawNavigations.get(defaultProfile.mobile_navigation)\n : undefined;\n\n // Filter themes to those referenced by the profile (by slug)\n const profileThemeSet = new Set(defaultProfile.themes);\n const profileThemes =\n profileThemeSet.size > 0\n ? Array.from(profileThemeSet)\n .map((slug) => themeSlugToRaw.get(slug))\n .filter((t): t is RawTheme => t != null)\n : rawThemes; // If no themes specified in profile, include all\n\n rawProfile = {\n name: defaultProfile.name,\n definition_id: definitionId,\n themes: profileThemes,\n ...(navigation ? { navigation } : {}),\n ...(mobileNavigation ? { mobile_navigation: mobileNavigation } : {}),\n };\n } else if (rawThemes.length > 0 || rawNavigations.size > 0) {\n // No profile files but we have themes/navs — create a synthetic profile\n const firstNav =\n rawNavigations.size > 0\n ? rawNavigations.values().next().value\n : undefined;\n\n rawProfile = {\n name: \"Default\",\n definition_id: definitionId,\n themes: rawThemes,\n ...(firstNav ? { navigation: firstNav } : {}),\n };\n }\n\n // 9. Return assembled manifest\n return {\n manifest: {\n definition_id: definitionId,\n published_version: 0,\n screens: rawScreens,\n ...(rawProfile ? { profile: rawProfile } : {}),\n },\n };\n}\n","/**\n * Vite plugin for local portal content development.\n *\n * Intercepts the manifest API request (`/api/fluid_os/definitions/active`)\n * and serves content assembled from the local `portal/` directory.\n * Watches portal files for changes and triggers a full page reload.\n */\n\nimport { join, resolve, sep } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { buildManifestFromLocalFiles } from \"./build-manifest.js\";\n\n/** Options for the portal dev plugin. */\nexport interface PortalDevPluginOptions {\n portalDir?: string;\n}\n\nconst DEBOUNCE_MS = 100;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function portalDevPlugin(options?: PortalDevPluginOptions): any {\n let portalDir: string;\n let cachedManifest: string | null = null;\n let buildInFlight: Promise<string> | null = null;\n\n return {\n name: \"fluid-portal-dev\",\n enforce: \"pre\" as const,\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n configResolved(config: any) {\n const root: string = config.root ?? process.cwd();\n portalDir = options?.portalDir\n ? resolve(root, options.portalDir)\n : join(root, \"portal\");\n },\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n configureServer(server: any) {\n const watchDir = portalDir;\n\n if (existsSync(watchDir)) {\n server.watcher.add(watchDir);\n\n let reloadTimer: ReturnType<typeof setTimeout> | null = null;\n\n const onFileChange = (filePath: string) => {\n if (!filePath.startsWith(watchDir)) return;\n\n cachedManifest = null;\n buildInFlight = null;\n\n console.log(\n `[fluid-portal-dev] File changed: ${filePath.replace(watchDir + sep, \"\")}`,\n );\n\n if (reloadTimer) clearTimeout(reloadTimer);\n reloadTimer = setTimeout(() => {\n reloadTimer = null;\n server.ws.send({ type: \"full-reload\", path: \"*\" });\n }, DEBOUNCE_MS);\n };\n\n server.watcher.on(\"change\", onFileChange);\n server.watcher.on(\"add\", onFileChange);\n server.watcher.on(\"unlink\", onFileChange);\n }\n\n // ── Middleware to intercept manifest requests ───────────────────\n server.middlewares.use(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (req: any, res: any, next: () => void) => {\n const url: string | undefined = req.url;\n\n if (!url?.includes(\"/api/fluid_os/definitions/active\")) {\n next();\n return;\n }\n\n (async () => {\n try {\n if (!existsSync(portalDir)) {\n // Fall through to the Vite proxy so requests reach the real\n // API when local portal content is unavailable (e.g. --skip-pull).\n next();\n return;\n }\n\n if (cachedManifest == null) {\n if (!buildInFlight) {\n buildInFlight = buildManifestFromLocalFiles(portalDir)\n .then((m) => {\n cachedManifest = JSON.stringify(m);\n buildInFlight = null;\n return cachedManifest;\n })\n .catch((err) => {\n buildInFlight = null; // allow retry on next request\n throw err;\n });\n }\n cachedManifest = await buildInFlight;\n }\n\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"X-Portal-Dev-Mode\": \"true\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(cachedManifest);\n } catch (err) {\n console.error(\"[fluid-portal-dev] Error building manifest:\", err);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error:\n err instanceof Error\n ? err.message\n : \"Failed to build manifest from local files\",\n }),\n );\n }\n })();\n },\n );\n },\n };\n}\n","/**\n * Standalone builder preview Vite plugin for use outside the SDK.\n *\n * NOTE: If using fluidManifestPlugin() from @fluid-app/portal-sdk/vite,\n * the builder preview is already included — you don't need this separately.\n * This export exists for advanced setups that use portalDevPlugin without the SDK.\n */\n\nimport 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>body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }</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\nexport function builderPreviewPlugin(): Plugin {\n let root: string;\n let configPath: string;\n let cssPath: string;\n\n return {\n name: \"fluid-builder-preview-standalone\",\n apply: \"serve\",\n\n configResolved(config: ResolvedConfig) {\n root = config.root;\n\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\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"],"mappings":";;;;;;;;;;;;;;;;;;AAuFA,SAAS,YAAY,KAAqB;AAGxC,QAFa,WAAW,MAAM,CAAC,OAAO,IAAI,CAAC,QAAQ,CAEvC,aAAa,EAAE;;;;;AAU7B,eAAe,aAAgB,UAAqC;AAClE,KAAI;EACF,MAAM,UAAU,MAAM,SAAS,UAAU,QAAQ;AACjD,SAAO,KAAK,MAAM,QAAQ;SACpB;AACN,SAAO;;;;;;;AAQX,eAAe,YACb,SACyC;AACzC,KAAI,CAAC,WAAW,QAAQ,CAAE,QAAO,EAAE;CAEnC,MAAM,UAAU,MAAM,QAAQ,SAAS,EAAE,eAAe,MAAM,CAAC;CAC/D,MAAM,UAA8B,EAAE;AAEtC,MAAK,MAAM,SAAS,SAAS;AAC3B,MAAI,CAAC,MAAM,QAAQ,IAAI,QAAQ,MAAM,KAAK,KAAK,QAAS;EAExD,MAAM,OAAO,SAAS,MAAM,MAAM,QAAQ;EAC1C,MAAM,OAAO,MAAM,aAAgB,KAAK,SAAS,MAAM,KAAK,CAAC;AAC7D,MAAI,QAAQ,KACV,SAAQ,KAAK,CAAC,MAAM,KAAK,CAAC;;AAI9B,QAAO;;;;;;AAWT,SAAS,qBACP,OACA,gBACqB;AACrB,QAAO,MAAM,KAAK,SAAS;EACzB,MAAM,UAA6B;GACjC,IAAI,KAAK;GACT,OAAO,KAAK,SAAS;GACrB,UAAU,KAAK,YAAY;GAC3B,UAAU,qBAAqB,KAAK,YAAY,EAAE,EAAE,eAAe;GACpE;AAED,MAAI,KAAK,QAAQ,KAAM,SAAQ,OAAO,KAAK;AAC3C,MAAI,KAAK,QAAQ,KAAM,SAAQ,OAAO,KAAK;AAC3C,MAAI,KAAK,UAAU,KAAM,SAAQ,SAAS,KAAK;AAC/C,MAAI,KAAK,aAAa,KAAM,SAAQ,YAAY,KAAK;AAGrD,MAAI,KAAK,UAAU,MAAM;GACvB,MAAM,WAAW,eAAe,IAAI,KAAK,OAAO;AAChD,OAAI,YAAY,KACd,SAAQ,YAAY;;AAIxB,SAAO;GACP;;;;;;;;AAaJ,eAAsB,4BACpB,WAC8B;CAM9B,MAAM,eAAe,YAAY,eAJd,MAAM,aACvB,KAAK,WAAW,kBAAkB,CACnC,GACkC,QAAQ,cACqB;CAGhE,MAAM,CAAC,eAAe,cAAc,YAAY,kBAC9C,MAAM,QAAQ,IAAI;EAChB,YAAyB,KAAK,WAAW,UAAU,CAAC;EACpD,YAAwB,KAAK,WAAW,SAAS,CAAC;EAClD,YAA6B,KAAK,WAAW,cAAc,CAAC;EAC5D,YAA0B,KAAK,WAAW,WAAW,CAAC;EACvD,CAAC;CAGJ,MAAM,iCAAiB,IAAI,KAAqB;AAChD,MAAK,MAAM,CAAC,SAAS,cACnB,gBAAe,IAAI,MAAM,YAAY,UAAU,OAAO,CAAC;CAIzD,MAAM,aAA0B,cAAc,KAAK,CAAC,MAAM,aAAa;EACrE,IAAI,eAAe,IAAI,KAAK;EAC5B,eAAe;EACf,MAAM,OAAO;EACb;EACA,gBAAgB,OAAO;EACxB,EAAE;CAGH,MAAM,iCAAiB,IAAI,KAAuB;AAClD,MAAK,MAAM,CAAC,MAAM,UAAU,aAC1B,gBAAe,IAAI,MAAM;EACvB,IAAI,YAAY,SAAS,OAAO;EAChC,MAAM,MAAM;EACZ,QAAQ,MAAM;EACd,QAAQ,MAAM;EACf,CAAC;CAEJ,MAAM,YAAY,MAAM,KAAK,eAAe,QAAQ,CAAC;CAGrD,MAAM,8BAAc,IAAI,KAAqB;AAC7C,MAAK,MAAM,CAAC,SAAS,WACnB,aAAY,IAAI,MAAM,YAAY,cAAc,OAAO,CAAC;CAI1D,MAAM,iCAAiB,IAAI,KAA4B;AACvD,MAAK,MAAM,CAAC,MAAM,QAAQ,WACxB,gBAAe,IAAI,MAAM;EACvB,IAAI,YAAY,IAAI,KAAK;EACzB,MAAM,IAAI;EACV,eAAe;EACf,kBAAkB,qBAChB,IAAI,oBAAoB,EAAE,EAC1B,eACD;EACF,CAAC;CAIJ,MAAM,iBACJ,eAAe,MAAM,GAAG,OAAO,EAAE,QAAQ,GAAG,MAC5C,eAAe,KAAK,MACpB;CAGF,IAAI;AACJ,KAAI,gBAAgB;EAElB,MAAM,aAAa,eAAe,aAC9B,eAAe,IAAI,eAAe,WAAW,GAC7C,KAAA;EAEJ,MAAM,mBAAmB,eAAe,oBACpC,eAAe,IAAI,eAAe,kBAAkB,GACpD,KAAA;EAGJ,MAAM,kBAAkB,IAAI,IAAI,eAAe,OAAO;EACtD,MAAM,gBACJ,gBAAgB,OAAO,IACnB,MAAM,KAAK,gBAAgB,CACxB,KAAK,SAAS,eAAe,IAAI,KAAK,CAAC,CACvC,QAAQ,MAAqB,KAAK,KAAK,GAC1C;AAEN,eAAa;GACX,MAAM,eAAe;GACrB,eAAe;GACf,QAAQ;GACR,GAAI,aAAa,EAAE,YAAY,GAAG,EAAE;GACpC,GAAI,mBAAmB,EAAE,mBAAmB,kBAAkB,GAAG,EAAE;GACpE;YACQ,UAAU,SAAS,KAAK,eAAe,OAAO,GAAG;EAE1D,MAAM,WACJ,eAAe,OAAO,IAClB,eAAe,QAAQ,CAAC,MAAM,CAAC,QAC/B,KAAA;AAEN,eAAa;GACX,MAAM;GACN,eAAe;GACf,QAAQ;GACR,GAAI,WAAW,EAAE,YAAY,UAAU,GAAG,EAAE;GAC7C;;AAIH,QAAO,EACL,UAAU;EACR,eAAe;EACf,mBAAmB;EACnB,SAAS;EACT,GAAI,aAAa,EAAE,SAAS,YAAY,GAAG,EAAE;EAC9C,EACF;;;;;;;;;;;AC/RH,MAAM,cAAc;AAGpB,SAAgB,gBAAgB,SAAuC;CACrE,IAAI;CACJ,IAAI,iBAAgC;CACpC,IAAI,gBAAwC;AAE5C,QAAO;EACL,MAAM;EACN,SAAS;EAGT,eAAe,QAAa;GAC1B,MAAM,OAAe,OAAO,QAAQ,QAAQ,KAAK;AACjD,eAAY,SAAS,YACjB,QAAQ,MAAM,QAAQ,UAAU,GAChC,KAAK,MAAM,SAAS;;EAI1B,gBAAgB,QAAa;GAC3B,MAAM,WAAW;AAEjB,OAAI,WAAW,SAAS,EAAE;AACxB,WAAO,QAAQ,IAAI,SAAS;IAE5B,IAAI,cAAoD;IAExD,MAAM,gBAAgB,aAAqB;AACzC,SAAI,CAAC,SAAS,WAAW,SAAS,CAAE;AAEpC,sBAAiB;AACjB,qBAAgB;AAEhB,aAAQ,IACN,oCAAoC,SAAS,QAAQ,WAAW,KAAK,GAAG,GACzE;AAED,SAAI,YAAa,cAAa,YAAY;AAC1C,mBAAc,iBAAiB;AAC7B,oBAAc;AACd,aAAO,GAAG,KAAK;OAAE,MAAM;OAAe,MAAM;OAAK,CAAC;QACjD,YAAY;;AAGjB,WAAO,QAAQ,GAAG,UAAU,aAAa;AACzC,WAAO,QAAQ,GAAG,OAAO,aAAa;AACtC,WAAO,QAAQ,GAAG,UAAU,aAAa;;AAI3C,UAAO,YAAY,KAEhB,KAAU,KAAU,SAAqB;AAGxC,QAAI,CAF4B,IAAI,KAE1B,SAAS,mCAAmC,EAAE;AACtD,WAAM;AACN;;AAGF,KAAC,YAAY;AACX,SAAI;AACF,UAAI,CAAC,WAAW,UAAU,EAAE;AAG1B,aAAM;AACN;;AAGF,UAAI,kBAAkB,MAAM;AAC1B,WAAI,CAAC,cACH,iBAAgB,4BAA4B,UAAU,CACnD,MAAM,MAAM;AACX,yBAAiB,KAAK,UAAU,EAAE;AAClC,wBAAgB;AAChB,eAAO;SACP,CACD,OAAO,QAAQ;AACd,wBAAgB;AAChB,cAAM;SACN;AAEN,wBAAiB,MAAM;;AAGzB,UAAI,UAAU,KAAK;OACjB,gBAAgB;OAChB,qBAAqB;OACrB,iBAAiB;OAClB,CAAC;AACF,UAAI,IAAI,eAAe;cAChB,KAAK;AACZ,cAAQ,MAAM,+CAA+C,IAAI;AACjE,UAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,UAAI,IACF,KAAK,UAAU,EACb,OACE,eAAe,QACX,IAAI,UACJ,6CACP,CAAC,CACH;;QAED;KAEP;;EAEJ;;;;AClHH,MAAM,mBAAmB;AACzB,MAAM,sBAAsB,OAAO;AAEnC,MAAM,WAAW;;;;;;;;;;;;;AAcjB,SAAgB,uBAA+B;CAC7C,IAAI;CACJ,IAAI;CACJ,IAAI;AAEJ,QAAO;EACL,MAAM;EACN,OAAO;EAEP,eAAe,QAAwB;AACrC,UAAO,OAAO;AAGd,gBADmB,CAAC,wBAAwB,mBAAmB,CAElD,MAAM,MAAM,WAAW,KAAK,MAAM,EAAE,CAAC,CAAC,IACjD;AAOF,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"}
1
+ {"version":3,"file":"vite-plugin.mjs","names":[],"sources":["../src/vite-plugin/build-manifest.ts","../src/vite-plugin/portal-dev-plugin.ts","../src/vite-plugin/builder-preview-plugin.ts","../src/vite-plugin/backend-dev-plugin.ts"],"sourcesContent":["/**\n * Build a RawManifestResponse from local portal/ files.\n *\n * Reads the portal directory structure produced by `fluid portal pull` and\n * assembles it into the same shape the SDK expects from the\n * `/api/fluid_os/definitions/active` endpoint, so the existing\n * `transformManifestToRepAppData()` pipeline works unchanged.\n */\n\nimport { readFile, readdir } from \"node:fs/promises\";\nimport { join, basename, extname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { createHash } from \"node:crypto\";\n\nimport type {\n LocalScreen,\n LocalTheme,\n LocalNavigation,\n LocalNavigationItem,\n LocalProfile,\n} from \"../utils/transform.js\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types matching the RawManifestResponse shape from\n// packages/portal/sdk/src/transforms/index.ts\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface RawNavigationItem {\n id: number;\n label: string;\n slug?: string;\n icon?: string;\n screen_id?: number;\n parent_id?: number;\n source?: string;\n position: number;\n children: RawNavigationItem[];\n}\n\ninterface RawNavigation {\n id: number;\n name?: string;\n definition_id: number;\n navigation_items?: RawNavigationItem[];\n}\n\ninterface RawTheme {\n id: number;\n config?: Record<string, unknown> | null;\n active?: boolean | null;\n name?: string | null;\n}\n\ninterface RawScreen {\n id: number;\n definition_id?: number;\n name?: string | null;\n slug?: string | null;\n component_tree?: unknown;\n}\n\ninterface RawManifestProfile {\n name?: string;\n definition_id: number;\n themes?: RawTheme[];\n navigation?: RawNavigation;\n mobile_navigation?: RawNavigation;\n}\n\ninterface RawManifestResponse {\n manifest: {\n definition_id: number;\n published_version?: number;\n screens?: RawScreen[];\n profile?: RawManifestProfile;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Synthetic ID generation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Generate a deterministic synthetic numeric ID from a string key.\n * Uses a CRC32-like hash so the same slug always produces the same ID\n * within a dev session, while avoiding collisions in practice.\n */\nfunction syntheticId(key: string): number {\n const hash = createHash(\"md5\").update(key).digest();\n // Read first 4 bytes as unsigned 32-bit integer, ensure positive\n return hash.readUInt32BE(0);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// File reading helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Read and parse a JSON file, returning null on failure.\n */\nasync function readJsonFile<T>(filePath: string): Promise<T | null> {\n try {\n const content = await readFile(filePath, \"utf-8\");\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Read all JSON files in a directory, returning an array of [slug, data] tuples.\n * Slug is the filename without extension.\n */\nasync function readJsonDir<T>(\n dirPath: string,\n): Promise<Array<[slug: string, data: T]>> {\n if (!existsSync(dirPath)) return [];\n\n const entries = await readdir(dirPath, { withFileTypes: true });\n const results: Array<[string, T]> = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || extname(entry.name) !== \".json\") continue;\n\n const slug = basename(entry.name, \".json\");\n const data = await readJsonFile<T>(join(dirPath, entry.name));\n if (data != null) {\n results.push([slug, data]);\n }\n }\n\n return results;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Navigation item conversion\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Convert local navigation items (which reference screens by slug)\n * back to the raw API format (which uses screen_id).\n */\nfunction toRawNavigationItems(\n items: LocalNavigationItem[],\n screenSlugToId: Map<string, number>,\n): RawNavigationItem[] {\n return items.map((item) => {\n const rawItem: RawNavigationItem = {\n id: item.id,\n label: item.label ?? \"Untitled\",\n position: item.position ?? 0,\n children: toRawNavigationItems(item.children ?? [], screenSlugToId),\n };\n\n if (item.slug != null) rawItem.slug = item.slug;\n if (item.icon != null) rawItem.icon = item.icon;\n if (item.source != null) rawItem.source = item.source;\n if (item.parent_id != null) rawItem.parent_id = item.parent_id;\n\n // Resolve screen slug -> synthetic screen ID\n if (item.screen != null) {\n const screenId = screenSlugToId.get(item.screen);\n if (screenId != null) {\n rawItem.screen_id = screenId;\n }\n }\n\n return rawItem;\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Main builder\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Build a `RawManifestResponse` from local portal/ files.\n *\n * The returned object matches the shape from `/api/fluid_os/definitions/active`\n * so the SDK's `transformManifestToRepAppData()` works without modification.\n */\nexport async function buildManifestFromLocalFiles(\n portalDir: string,\n): Promise<RawManifestResponse> {\n // 1. Read definition.json\n const definition = await readJsonFile<{ name?: string }>(\n join(portalDir, \"definition.json\"),\n );\n const definitionName = definition?.name ?? \"Local Dev\";\n const definitionId = syntheticId(`definition:${definitionName}`);\n\n // 2. Read all resource files in parallel\n const [screenEntries, themeEntries, navEntries, profileEntries] =\n await Promise.all([\n readJsonDir<LocalScreen>(join(portalDir, \"screens\")),\n readJsonDir<LocalTheme>(join(portalDir, \"themes\")),\n readJsonDir<LocalNavigation>(join(portalDir, \"navigations\")),\n readJsonDir<LocalProfile>(join(portalDir, \"profiles\")),\n ]);\n\n // 3. Build screen slug -> synthetic ID map\n const screenSlugToId = new Map<string, number>();\n for (const [slug] of screenEntries) {\n screenSlugToId.set(slug, syntheticId(`screen:${slug}`));\n }\n\n // 4. Build raw screens\n const rawScreens: RawScreen[] = screenEntries.map(([slug, screen]) => ({\n id: screenSlugToId.get(slug)!,\n definition_id: definitionId,\n name: screen.name,\n slug,\n component_tree: screen.component_tree,\n }));\n\n // 5. Build raw themes (keyed by slug for profile lookup)\n const themeSlugToRaw = new Map<string, RawTheme>();\n for (const [slug, theme] of themeEntries) {\n themeSlugToRaw.set(slug, {\n id: syntheticId(`theme:${slug}`),\n name: theme.name,\n config: theme.config,\n active: theme.active,\n });\n }\n const rawThemes = Array.from(themeSlugToRaw.values());\n\n // 6. Build navigation slug -> synthetic ID map\n const navSlugToId = new Map<string, number>();\n for (const [slug] of navEntries) {\n navSlugToId.set(slug, syntheticId(`navigation:${slug}`));\n }\n\n // Build raw navigations (keyed by slug for profile lookup)\n const rawNavigations = new Map<string, RawNavigation>();\n for (const [slug, nav] of navEntries) {\n rawNavigations.set(slug, {\n id: navSlugToId.get(slug)!,\n name: nav.name,\n definition_id: definitionId,\n navigation_items: toRawNavigationItems(\n nav.navigation_items ?? [],\n screenSlugToId,\n ),\n });\n }\n\n // 7. Find the default profile (or use the first one)\n const defaultProfile =\n profileEntries.find(([, p]) => p.default)?.[1] ??\n profileEntries[0]?.[1] ??\n null;\n\n // 8. Assemble the profile for the manifest\n let rawProfile: RawManifestProfile | undefined;\n if (defaultProfile) {\n // Resolve navigation slugs to raw navigation objects\n const navigation = defaultProfile.navigation\n ? rawNavigations.get(defaultProfile.navigation)\n : undefined;\n\n const mobileNavigation = defaultProfile.mobile_navigation\n ? rawNavigations.get(defaultProfile.mobile_navigation)\n : undefined;\n\n // Filter themes to those referenced by the profile (by slug)\n const profileThemeSet = new Set(defaultProfile.themes);\n const profileThemes =\n profileThemeSet.size > 0\n ? Array.from(profileThemeSet)\n .map((slug) => themeSlugToRaw.get(slug))\n .filter((t): t is RawTheme => t != null)\n : rawThemes; // If no themes specified in profile, include all\n\n rawProfile = {\n name: defaultProfile.name,\n definition_id: definitionId,\n themes: profileThemes,\n ...(navigation ? { navigation } : {}),\n ...(mobileNavigation ? { mobile_navigation: mobileNavigation } : {}),\n };\n } else if (rawThemes.length > 0 || rawNavigations.size > 0) {\n // No profile files but we have themes/navs — create a synthetic profile\n const firstNav =\n rawNavigations.size > 0\n ? rawNavigations.values().next().value\n : undefined;\n\n rawProfile = {\n name: \"Default\",\n definition_id: definitionId,\n themes: rawThemes,\n ...(firstNav ? { navigation: firstNav } : {}),\n };\n }\n\n // 9. Return assembled manifest\n return {\n manifest: {\n definition_id: definitionId,\n published_version: 0,\n screens: rawScreens,\n ...(rawProfile ? { profile: rawProfile } : {}),\n },\n };\n}\n","/**\n * Vite plugin for local portal content development.\n *\n * Intercepts the manifest API request (`/api/fluid_os/definitions/active`)\n * and serves content assembled from the local `portal/` directory.\n * Watches portal files for changes and triggers a full page reload.\n */\n\nimport { join, resolve, sep } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { buildManifestFromLocalFiles } from \"./build-manifest.js\";\n\n/** Options for the portal dev plugin. */\nexport interface PortalDevPluginOptions {\n portalDir?: string;\n}\n\nconst DEBOUNCE_MS = 100;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function portalDevPlugin(options?: PortalDevPluginOptions): any {\n let portalDir: string;\n let cachedManifest: string | null = null;\n let buildInFlight: Promise<string> | null = null;\n\n return {\n name: \"fluid-portal-dev\",\n enforce: \"pre\" as const,\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n configResolved(config: any) {\n const root: string = config.root ?? process.cwd();\n portalDir = options?.portalDir\n ? resolve(root, options.portalDir)\n : join(root, \"portal\");\n },\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n configureServer(server: any) {\n const watchDir = portalDir;\n\n if (existsSync(watchDir)) {\n server.watcher.add(watchDir);\n\n let reloadTimer: ReturnType<typeof setTimeout> | null = null;\n\n const onFileChange = (filePath: string) => {\n if (!filePath.startsWith(watchDir)) return;\n\n cachedManifest = null;\n buildInFlight = null;\n\n console.log(\n `[fluid-portal-dev] File changed: ${filePath.replace(watchDir + sep, \"\")}`,\n );\n\n if (reloadTimer) clearTimeout(reloadTimer);\n reloadTimer = setTimeout(() => {\n reloadTimer = null;\n server.ws.send({ type: \"full-reload\", path: \"*\" });\n }, DEBOUNCE_MS);\n };\n\n server.watcher.on(\"change\", onFileChange);\n server.watcher.on(\"add\", onFileChange);\n server.watcher.on(\"unlink\", onFileChange);\n }\n\n // ── Middleware to intercept manifest requests ───────────────────\n server.middlewares.use(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (req: any, res: any, next: () => void) => {\n const url: string | undefined = req.url;\n\n if (!url?.includes(\"/api/fluid_os/definitions/active\")) {\n next();\n return;\n }\n\n (async () => {\n try {\n if (!existsSync(portalDir)) {\n // Fall through to the Vite proxy so requests reach the real\n // API when local portal content is unavailable (e.g. --skip-pull).\n next();\n return;\n }\n\n if (cachedManifest == null) {\n if (!buildInFlight) {\n buildInFlight = buildManifestFromLocalFiles(portalDir)\n .then((m) => {\n cachedManifest = JSON.stringify(m);\n buildInFlight = null;\n return cachedManifest;\n })\n .catch((err) => {\n buildInFlight = null; // allow retry on next request\n throw err;\n });\n }\n cachedManifest = await buildInFlight;\n }\n\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"X-Portal-Dev-Mode\": \"true\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(cachedManifest);\n } catch (err) {\n console.error(\"[fluid-portal-dev] Error building manifest:\", err);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error:\n err instanceof Error\n ? err.message\n : \"Failed to build manifest from local files\",\n }),\n );\n }\n })();\n },\n );\n },\n };\n}\n","/**\n * Standalone builder preview Vite plugin for use outside the SDK.\n *\n * NOTE: If using fluidManifestPlugin() from @fluid-app/portal-sdk/vite,\n * the builder preview is already included — you don't need this separately.\n * This export exists for advanced setups that use portalDevPlugin without the SDK.\n */\n\nimport 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>body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }</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\nexport function builderPreviewPlugin(): Plugin {\n let root: string;\n let configPath: string;\n let cssPath: string;\n\n return {\n name: \"fluid-builder-preview-standalone\",\n apply: \"serve\",\n\n configResolved(config: ResolvedConfig) {\n root = config.root;\n\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\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 } from \"vite\";\n\nexport interface BackendDevPluginOptions {\n /** Entry module path relative to project root. @default \"/src/main.tsx\" */\n entry?: string;\n /** Production chunk filenames to stub as empty ES modules. */\n chunks?: string[];\n}\n\n/**\n * Serves dev-compatible responses at production asset paths so the Rails\n * backend can point `PORTAL_CDN_BASE` at `http://localhost:5173` and get\n * HMR + live source without any template changes.\n *\n * Handles:\n * - `/portal.js` → ESM shim that loads `@vite/client` + the real entry\n * - `/portal.css` → empty (Vite injects CSS via JS in dev)\n * - `/vendor.js`, `/query.js` → empty ES modules (no chunks in dev)\n */\nexport function backendDevPlugin(options?: BackendDevPluginOptions): Plugin {\n const entry = options?.entry ?? \"/src/main.tsx\";\n const chunks = options?.chunks ?? [\"vendor.js\", \"query.js\"];\n\n return {\n name: \"fluid-backend-dev\",\n apply: \"serve\",\n\n // Register BEFORE Vite's SPA fallback so these paths don't\n // get caught by the index.html catch-all.\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const raw = req.url ?? \"\";\n const qIndex = raw.indexOf(\"?\");\n const url = qIndex === -1 ? raw : raw.slice(0, qIndex);\n\n if (url === \"/portal.js\") {\n serve(\n res,\n \"application/javascript\",\n [\n `import RefreshRuntime from \"/@react-refresh\";`,\n `import \"/@vite/client\";`,\n `RefreshRuntime.injectIntoGlobalHook(window);`,\n `window.$RefreshReg$ = () => {};`,\n `window.$RefreshSig$ = () => (type) => type;`,\n `window.__vite_plugin_react_preamble_installed__ = true;`,\n `await import(\"${entry}\");`,\n ].join(\"\\n\"),\n );\n return;\n }\n\n if (url === \"/portal.css\") {\n serve(res, \"text/css\", \"/* dev: CSS injected via JS */\");\n return;\n }\n\n const chunkName = url.startsWith(\"/\") ? url.slice(1) : url;\n if (chunks.includes(chunkName)) {\n serve(res, \"application/javascript\", \"export {};\");\n return;\n }\n\n next();\n });\n },\n };\n}\n\nfunction serve(\n res: import(\"http\").ServerResponse,\n contentType: string,\n body: string,\n) {\n res.setHeader(\"Content-Type\", `${contentType}; charset=utf-8`);\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.statusCode = 200;\n res.end(body);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-portal",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Fluid CLI plugin for building portal applications",
5
5
  "files": [
6
6
  "dist",
@@ -32,7 +32,7 @@
32
32
  "p-limit": "^7.3.0",
33
33
  "prompts": "^2.4.2",
34
34
  "tsx": "^4.21.0",
35
- "@fluid-app/fluid-cli": "0.1.4"
35
+ "@fluid-app/fluid-cli": "0.1.5"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@swc/core": "^1.15.18",
@@ -44,8 +44,8 @@
44
44
  "tsdown": "^0.21.0",
45
45
  "typescript": "^5",
46
46
  "vite": "^6.0.0",
47
- "@fluid-app/typescript-config": "0.0.0",
48
- "@fluid-app/fluidos-api-client": "0.1.0"
47
+ "@fluid-app/fluidos-api-client": "0.1.0",
48
+ "@fluid-app/typescript-config": "0.0.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=18.0.0"
@@ -0,0 +1,3 @@
1
+ {
2
+ "profile": "{{profileName}}"
3
+ }
@@ -1 +1,6 @@
1
1
  VITE_API_URL=https://api.fluid.app
2
+
3
+ # Optional: override the Fluid CLI auth token for this project.
4
+ # Takes effect when running `pnpm pull`, `pnpm push`, etc.
5
+ # Alternatively, set the "profile" field in .fluidrc (committed to the repo).
6
+ # FLUID_TOKEN=
@@ -8,21 +8,25 @@
8
8
  "build": "tsc -b && vite build",
9
9
  "preview": "vite preview",
10
10
  "typecheck": "tsc --noEmit",
11
- "lint": "oxlint"
11
+ "lint": "oxlint",
12
+ "pull": "fluid portal pull",
13
+ "push": "fluid portal push",
14
+ "widget:create": "fluid portal widget create"
12
15
  },
13
16
  "dependencies": {
14
- "@fluid-app/portal-preview": "{{sdkVersion}}",
15
17
  "@fluid-app/portal-sdk": "{{sdkVersion}}",
16
- "@fluid-app/ui-primitives": "{{sdkVersion}}",
18
+ "@fluid-app/ui-primitives": "^0.1.0",
17
19
  "@tanstack/react-query": "^5.90.0",
18
20
  "react": "^19.0.0",
19
21
  "react-dom": "^19.0.0",
20
22
  "react-hook-form": "^7.55.0",
21
23
  "@hookform/resolvers": "^4.1.3",
22
- "zod": "^3.24.0"
24
+ "zod": "^3.24.0",
25
+ "zustand": "^5.0.0"
23
26
  },
24
27
  "devDependencies": {
25
- "@fluid-app/fluid-cli-portal": "{{sdkVersion}}",
28
+ "@fluid-app/fluid-cli": "{{cliVersion}}",
29
+ "@fluid-app/fluid-cli-portal": "{{cliVersion}}",
26
30
  "@tailwindcss/vite": "^4.0.0",
27
31
  "@types/react": "^19.0.0",
28
32
  "@types/react-dom": "^19.0.0",