@fluid-app/fluid-cli-widget 0.1.27 → 0.1.28

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.
@@ -1,14 +1,15 @@
1
- import { validateSingleSourceWidgetPackage } from "@fluid-app/fluid-cli-portal";
1
+ import { i as createWidgetPackageDevelopmentDescriptor } from "../src-CVadzcYM.mjs";
2
2
  //#region src/vite/plugin.ts
3
3
  function fluidWidgetDevPlugin() {
4
4
  return {
5
5
  name: "fluid-widget-dev-routes",
6
6
  apply: "serve",
7
+ config: createWidgetDevServerConfig,
7
8
  configureServer(server) {
8
9
  server.middlewares.use(async (request, response, next) => {
9
10
  const pathname = (request.url ?? "").split("?")[0] ?? "";
10
11
  if (pathname === "/__runtime-entry__" || pathname === "/__runtime-entry__/") {
11
- sendJavaScript(request, response, buildRuntimeEntryBootstrapScript(WIDGET_ENTRY_MODULE_PATH));
12
+ sendJavaScript(request, response, buildRuntimeEntryBootstrapScript(WIDGET_ENTRY_MODULE_PATH, getRequestOrigin(request)));
12
13
  return;
13
14
  }
14
15
  if (pathname === "/__widget-packages__" || pathname === "/__widget-packages__/") {
@@ -20,6 +21,21 @@ function fluidWidgetDevPlugin() {
20
21
  }
21
22
  };
22
23
  }
24
+ /**
25
+ * Widget projects intentionally have no HTML entrypoint, so Vite cannot
26
+ * discover the worker's dependency graph during its normal startup scan.
27
+ * Without an explicit entry, the first live-preview request discovers and
28
+ * optimizes the portal worker after the server reports ready. Vite reloads
29
+ * the worker mid-mount, closing its message thread and stranding the preview
30
+ * in a permanent error state.
31
+ *
32
+ * Pre-optimizing the worker entry makes the initial graph stable before a
33
+ * preview can connect. Vite follows this entry's imports, so React and the
34
+ * Remote DOM worker runtime are optimized in the same startup pass.
35
+ */
36
+ function createWidgetDevServerConfig() {
37
+ return { optimizeDeps: { include: ["@fluid-app/portal-sdk/widgets/worker"] } };
38
+ }
23
39
  /** Source-relative path of a widget project's entry module. The dev
24
40
  * scaffold always places it here (see the widget template) — this is
25
41
  * the one thing the bootstrap script below needs from the plugin
@@ -73,11 +89,19 @@ const WIDGET_ENTRY_MODULE_PATH = "/src/index.ts";
73
89
  * import the entry module (syntax error, missing dependency, …)
74
90
  * must reject the worker's module evaluation loudly, not vanish
75
91
  * the way a fire-and-forget `void import` did.
92
+ * 4. **Absolute dev-server imports.** Mist and portal previews load the
93
+ * worker from another origin, so the host wraps this entry in a blob URL.
94
+ * Root-relative specifiers inside a blob module resolve against the host
95
+ * page (or fail to resolve at all), not this Vite server. Anchoring both
96
+ * imports to the request origin keeps the entire dev module graph on the
97
+ * widget server.
76
98
  *
77
99
  * Dev-only: this only ever serves from `configureServer` (the
78
100
  * plugin's `apply: "serve"` gate), never a built/published package.
79
101
  */
80
- function buildRuntimeEntryBootstrapScript(entryPath) {
102
+ function buildRuntimeEntryBootstrapScript(entryPath, origin) {
103
+ const refreshRuntimeUrl = new URL("/@react-refresh", origin).toString();
104
+ const entryUrl = new URL(entryPath, origin).toString();
81
105
  return [
82
106
  "// Dev-only worker bootstrap — see buildRuntimeEntryBootstrapScript",
83
107
  "// in packages/cli/widget/src/vite/plugin.ts for why each piece",
@@ -88,7 +112,7 @@ function buildRuntimeEntryBootstrapScript(entryPath) {
88
112
  "self.addEventListener(\"message\", __buf);",
89
113
  "if (typeof window === \"undefined\") { globalThis.window = globalThis; }",
90
114
  "try {",
91
- " const { default: RefreshRuntime } = await import(\"/@react-refresh\");",
115
+ ` const { default: RefreshRuntime } = await import(${JSON.stringify(refreshRuntimeUrl)});`,
92
116
  " RefreshRuntime.injectIntoGlobalHook(window);",
93
117
  " window.$RefreshReg$ = () => {};",
94
118
  " window.$RefreshSig$ = () => (type) => type;",
@@ -97,7 +121,7 @@ function buildRuntimeEntryBootstrapScript(entryPath) {
97
121
  " // No @vitejs/plugin-react in this widget project (no",
98
122
  " // /@react-refresh virtual module) — nothing to preinstall.",
99
123
  "}",
100
- `await import(${JSON.stringify(entryPath)});`,
124
+ `await import(${JSON.stringify(entryUrl)});`,
101
125
  "self.removeEventListener(\"message\", __buf);",
102
126
  "for (const data of __q) {",
103
127
  " self.dispatchEvent(new MessageEvent(\"message\", { data }));",
@@ -105,45 +129,19 @@ function buildRuntimeEntryBootstrapScript(entryPath) {
105
129
  ""
106
130
  ].join("\n");
107
131
  }
108
- function createDevWidgetPackageDescriptors(configModule, origin) {
109
- const validation = validateSingleSourceWidgetPackage(readSourcePackages(configModule), { owner: "droplet" });
110
- if (!validation.success || !validation.value) throw new Error(validation.errors.map((error) => error.message).join("\n") || "Widget package validation failed.");
111
- return [validatedPackageToDescriptor(validation.value, origin)];
112
- }
113
- function validatedPackageToDescriptor(validated, origin) {
114
- const cssUrls = validated.sourcePackage.cssUrls ?? [];
115
- return {
116
- manifestVersion: 1,
117
- runtime: "remote-dom",
118
- packageId: validated.packageId,
119
- source: validated.owner,
120
- version: validated.version,
121
- workerEntryUrl: new URL("/__runtime-entry__", origin).href,
122
- cssUrls: cssUrls.length > 0 ? cssUrls.map((url) => toDevCssUrl(url, origin)) : [toDevCssUrl("/styles.css", origin)],
123
- assetUrls: [],
124
- widgets: validated.widgets.map(({ name: _name, ...widget }) => widget)
125
- };
126
- }
127
132
  /**
128
- * Vite's dev server serves a `.css` source file as a JS module by
129
- * default (so `import "./x.css"` hot-reloads) — fetching that URL
130
- * directly, as the widget-runtime host does to inject real
131
- * `<style>`/adopted-stylesheet content into a widget's shadow root,
132
- * returns `text/javascript`, not CSS. Vite's own dev server
133
- * recognizes a `?direct` query as a request for the genuine `text/css`
134
- * response, bypassing the JS-module transform.
135
- *
136
- * This descriptor builder only ever runs inside `configureServer`
137
- * (the plugin's `apply: "serve"` gate — see `fluidWidgetDevPlugin`
138
- * above), i.e. only for the dev preview's `/__widget-packages__`
139
- * response. A published/built widget package's `cssUrls` are produced
140
- * by a completely different path (the CLI's build pipeline) and point
141
- * at real static `.css` files with no Vite transform in front of
142
- * them, so this suffix can never leak into a production descriptor.
133
+ * The shared development descriptor workflow appends Vite's `?direct` query
134
+ * to CSS URLs so preview hosts receive real CSS instead of JS style modules.
135
+ * Published descriptors use a separate build path and never receive it.
143
136
  */
144
- function toDevCssUrl(url, origin) {
145
- const { href } = new URL(url, origin);
146
- return href.includes("?") ? `${href}&direct` : `${href}?direct`;
137
+ function createDevWidgetPackageDescriptors(configModule, origin) {
138
+ return [createWidgetPackageDevelopmentDescriptor({
139
+ sourceModule: configModule,
140
+ owner: "droplet",
141
+ origin,
142
+ workerEntryPath: "/__runtime-entry__",
143
+ defaultCssPath: "/styles.css"
144
+ })];
147
145
  }
148
146
  async function sendWidgetPackages(server, request, response) {
149
147
  if (handleOptions(request, response)) return;
@@ -154,12 +152,6 @@ async function sendWidgetPackages(server, request, response) {
154
152
  sendJson(response, { error: String(error) }, 500);
155
153
  }
156
154
  }
157
- function readSourcePackages(configModule) {
158
- const packages = configModule["widgetPackages"];
159
- if (Array.isArray(packages) && packages.length > 0) return packages;
160
- const widgetPackage = configModule["widgetPackage"];
161
- return widgetPackage === void 0 ? [] : [widgetPackage];
162
- }
163
155
  function sendJavaScript(request, response, source) {
164
156
  if (handleOptions(request, response)) return;
165
157
  setCorsHeaders(response);
@@ -195,6 +187,6 @@ function readHeader(value) {
195
187
  return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : void 0;
196
188
  }
197
189
  //#endregion
198
- export { buildRuntimeEntryBootstrapScript, createDevWidgetPackageDescriptors, fluidWidgetDevPlugin };
190
+ export { buildRuntimeEntryBootstrapScript, createDevWidgetPackageDescriptors, createWidgetDevServerConfig, fluidWidgetDevPlugin };
199
191
 
200
192
  //# sourceMappingURL=plugin.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.mjs","names":[],"sources":["../../src/vite/plugin.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport {\n validateSingleSourceWidgetPackage,\n type RemoteDomWidgetPackageDescriptor,\n type ValidatedWidgetPackageBuild,\n} from \"@fluid-app/fluid-cli-portal\";\nimport type { Plugin, ViteDevServer } from \"vite\";\n\nexport function fluidWidgetDevPlugin(): Plugin {\n return {\n name: \"fluid-widget-dev-routes\",\n apply: \"serve\",\n configureServer(server) {\n server.middlewares.use(async (request, response, next): Promise<void> => {\n const pathname = (request.url ?? \"\").split(\"?\")[0] ?? \"\";\n\n if (\n pathname === \"/__runtime-entry__\" ||\n pathname === \"/__runtime-entry__/\"\n ) {\n sendJavaScript(\n request,\n response,\n buildRuntimeEntryBootstrapScript(WIDGET_ENTRY_MODULE_PATH),\n );\n return;\n }\n\n if (\n pathname === \"/__widget-packages__\" ||\n pathname === \"/__widget-packages__/\"\n ) {\n await sendWidgetPackages(server, request, response);\n return;\n }\n\n next();\n });\n },\n };\n}\n\n/** Source-relative path of a widget project's entry module. The dev\n * scaffold always places it here (see the widget template) — this is\n * the one thing the bootstrap script below needs from the plugin\n * that isn't itself part of the race/preamble fix. */\nconst WIDGET_ENTRY_MODULE_PATH = \"/src/index.ts\";\n\n/**\n * Builds the `/__runtime-entry__` worker bootstrap. Runtime-verified\n * against a real widget dev server + `@fluid-app/widget-runtime` host\n * in a Playwright harness (styled Leaderboard widget rendering,\n * mutation records flowing, `mounted.ready` resolving) — every piece\n * below exists to fix something that reproduced in that harness:\n *\n * 1. **Message-buffer race (the silent-mount killer).** The host\n * (widget-runtime) posts the \"mount\" message immediately after\n * `new Worker(...)`. A production-built worker bundle evaluates\n * synchronously and never races that message. In dev, though, the\n * worker's module graph loads asynchronously over many HTTP\n * requests (this bootstrap script, then the entry module, then\n * everything it imports, each a separate fetch) — so the \"mount\"\n * message can be dispatched and consumed by the Worker's default\n * (listener-less) handling before the widget entry's own\n * `message` listener, wired up deep inside the awaited import\n * below, is ever attached. The message is dropped silently, with\n * no error anywhere — the old `void import(\"/src/index.ts\")` also\n * swallowed the import's own rejections, compounding the silence.\n * Traced live: host→worker \"mount\" posts, zero worker→host replies\n * until this buffer-and-replay was added; after it, mutation\n * records flow and `mounted.ready` resolves. Fix: buffer every\n * message from the instant the worker boots (before anything else\n * runs), then replay the buffer, in order, once the entry module\n * has finished importing and has had the chance to attach its\n * real listener.\n * 2. **React-refresh preamble.** `@vitejs/plugin-react` injects a\n * preamble into every dev-transformed `.tsx` module referencing\n * `window.$RefreshReg$` / `$RefreshSig$`. A Worker global scope has\n * no `window` and no preamble, so importing a react-based widget\n * entry throws `window is not defined`, and — once `window` is\n * aliased to `globalThis` — throws again on the bare\n * `$RefreshSig$` alias. Installing the canonical preamble\n * (`RefreshRuntime.injectIntoGlobalHook` + no-op `$RefreshReg$` /\n * `$RefreshSig$`) before the entry import fixes both. The\n * `window = globalThis` alias was checked against widget-runtime's\n * worker lockdown / scope detection (which is\n * `globalThis.postMessage`-based) — no conflict. `/@react-refresh`\n * is `@vitejs/plugin-react`'s stable virtual module id, but a\n * widget project isn't guaranteed to use the react plugin, so the\n * whole block is try/catch-guarded — a project with no\n * `/@react-refresh` module (no react plugin) boots straight\n * through to the entry import unaffected.\n * 3. **`await import`, not `void import`.** A genuine failure to\n * import the entry module (syntax error, missing dependency, …)\n * must reject the worker's module evaluation loudly, not vanish\n * the way a fire-and-forget `void import` did.\n *\n * Dev-only: this only ever serves from `configureServer` (the\n * plugin's `apply: \"serve\"` gate), never a built/published package.\n */\nexport function buildRuntimeEntryBootstrapScript(entryPath: string): string {\n return [\n \"// Dev-only worker bootstrap — see buildRuntimeEntryBootstrapScript\",\n \"// in packages/cli/widget/src/vite/plugin.ts for why each piece\",\n \"// here exists (message-buffer race, react-refresh preamble,\",\n \"// await-not-void import).\",\n \"const __q = [];\",\n \"const __buf = (e) => { __q.push(e.data); };\",\n 'self.addEventListener(\"message\", __buf);',\n 'if (typeof window === \"undefined\") { globalThis.window = globalThis; }',\n \"try {\",\n ' const { default: RefreshRuntime } = await import(\"/@react-refresh\");',\n \" RefreshRuntime.injectIntoGlobalHook(window);\",\n \" window.$RefreshReg$ = () => {};\",\n \" window.$RefreshSig$ = () => (type) => type;\",\n \" window.__vite_plugin_react_preamble_installed__ = true;\",\n \"} catch {\",\n \" // No @vitejs/plugin-react in this widget project (no\",\n \" // /@react-refresh virtual module) — nothing to preinstall.\",\n \"}\",\n `await import(${JSON.stringify(entryPath)});`,\n 'self.removeEventListener(\"message\", __buf);',\n \"for (const data of __q) {\",\n ' self.dispatchEvent(new MessageEvent(\"message\", { data }));',\n \"}\",\n \"\",\n ].join(\"\\n\");\n}\n\nexport function createDevWidgetPackageDescriptors(\n configModule: Readonly<Record<string, unknown>>,\n origin: string,\n): readonly RemoteDomWidgetPackageDescriptor[] {\n const validation = validateSingleSourceWidgetPackage(\n readSourcePackages(configModule),\n { owner: \"droplet\" },\n );\n if (!validation.success || !validation.value) {\n throw new Error(\n validation.errors.map((error) => error.message).join(\"\\n\") ||\n \"Widget package validation failed.\",\n );\n }\n\n return [validatedPackageToDescriptor(validation.value, origin)];\n}\n\nfunction validatedPackageToDescriptor(\n validated: ValidatedWidgetPackageBuild,\n origin: string,\n): RemoteDomWidgetPackageDescriptor {\n const cssUrls = validated.sourcePackage.cssUrls ?? [];\n return {\n manifestVersion: 1,\n runtime: \"remote-dom\",\n packageId: validated.packageId,\n source: validated.owner,\n version: validated.version,\n workerEntryUrl: new URL(\"/__runtime-entry__\", origin).href,\n cssUrls:\n cssUrls.length > 0\n ? cssUrls.map((url) => toDevCssUrl(url, origin))\n : [toDevCssUrl(\"/styles.css\", origin)],\n assetUrls: [],\n widgets: validated.widgets.map(({ name: _name, ...widget }) => widget),\n };\n}\n\n/**\n * Vite's dev server serves a `.css` source file as a JS module by\n * default (so `import \"./x.css\"` hot-reloads) — fetching that URL\n * directly, as the widget-runtime host does to inject real\n * `<style>`/adopted-stylesheet content into a widget's shadow root,\n * returns `text/javascript`, not CSS. Vite's own dev server\n * recognizes a `?direct` query as a request for the genuine `text/css`\n * response, bypassing the JS-module transform.\n *\n * This descriptor builder only ever runs inside `configureServer`\n * (the plugin's `apply: \"serve\"` gate — see `fluidWidgetDevPlugin`\n * above), i.e. only for the dev preview's `/__widget-packages__`\n * response. A published/built widget package's `cssUrls` are produced\n * by a completely different path (the CLI's build pipeline) and point\n * at real static `.css` files with no Vite transform in front of\n * them, so this suffix can never leak into a production descriptor.\n */\nfunction toDevCssUrl(url: string, origin: string): string {\n const { href } = new URL(url, origin);\n return href.includes(\"?\") ? `${href}&direct` : `${href}?direct`;\n}\n\nasync function sendWidgetPackages(\n server: ViteDevServer,\n request: IncomingMessage,\n response: ServerResponse,\n): Promise<void> {\n if (handleOptions(request, response)) return;\n\n try {\n const configModule = (await server.ssrLoadModule(\n \"/fluid.widget.config.ts\",\n )) as Record<string, unknown>;\n sendJson(\n response,\n createDevWidgetPackageDescriptors(\n configModule,\n getRequestOrigin(request),\n ),\n );\n } catch (error) {\n server.config.logger.error(\n `[fluid] Failed to load widget packages: ${String(error)}`,\n );\n sendJson(response, { error: String(error) }, 500);\n }\n}\n\nfunction readSourcePackages(\n configModule: Readonly<Record<string, unknown>>,\n): readonly unknown[] {\n const packages = configModule[\"widgetPackages\"];\n if (Array.isArray(packages) && packages.length > 0) return packages;\n\n const widgetPackage = configModule[\"widgetPackage\"];\n return widgetPackage === undefined ? [] : [widgetPackage];\n}\n\nfunction sendJavaScript(\n request: IncomingMessage,\n response: ServerResponse,\n source: string,\n): void {\n if (handleOptions(request, response)) return;\n setCorsHeaders(response);\n response.setHeader(\"Content-Type\", \"text/javascript\");\n response.statusCode = 200;\n response.end(source);\n}\n\nfunction sendJson(\n response: ServerResponse,\n body: unknown,\n statusCode = 200,\n): void {\n setCorsHeaders(response);\n response.setHeader(\"Content-Type\", \"application/json\");\n response.statusCode = statusCode;\n response.end(JSON.stringify(body, null, 2));\n}\n\nfunction handleOptions(\n request: IncomingMessage,\n response: ServerResponse,\n): boolean {\n if (request.method !== \"OPTIONS\") return false;\n setCorsHeaders(response);\n response.statusCode = 204;\n response.end();\n return true;\n}\n\nfunction setCorsHeaders(response: ServerResponse): void {\n response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n response.setHeader(\"Access-Control-Allow-Methods\", \"GET, HEAD, OPTIONS\");\n response.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization\",\n );\n}\n\nfunction getRequestOrigin(request: IncomingMessage): string {\n const host = readHeader(request.headers.host);\n if (!host) throw new Error(\"Widget dev request is missing the Host header.\");\n\n const forwardedProtocol = readHeader(request.headers[\"x-forwarded-proto\"]);\n const protocol = forwardedProtocol === \"https\" ? \"https\" : \"http\";\n return `${protocol}://${host}`;\n}\n\nfunction readHeader(\n value: string | readonly string[] | undefined,\n): string | undefined {\n const candidate = Array.isArray(value) ? value[0] : value;\n return typeof candidate === \"string\" && candidate.trim().length > 0\n ? candidate.trim()\n : undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.mjs","names":[],"sources":["../../src/vite/plugin.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport {\n createWidgetPackageDevelopmentDescriptor,\n type RemoteDomWidgetPackageDescriptor,\n} from \"@fluid-app/fluid-cli-widget-tooling\";\nimport type { Plugin, UserConfig, ViteDevServer } from \"vite\";\n\nexport function fluidWidgetDevPlugin(): Plugin {\n return {\n name: \"fluid-widget-dev-routes\",\n apply: \"serve\",\n config: createWidgetDevServerConfig,\n configureServer(server) {\n server.middlewares.use(async (request, response, next): Promise<void> => {\n const pathname = (request.url ?? \"\").split(\"?\")[0] ?? \"\";\n\n if (\n pathname === \"/__runtime-entry__\" ||\n pathname === \"/__runtime-entry__/\"\n ) {\n sendJavaScript(\n request,\n response,\n buildRuntimeEntryBootstrapScript(\n WIDGET_ENTRY_MODULE_PATH,\n getRequestOrigin(request),\n ),\n );\n return;\n }\n\n if (\n pathname === \"/__widget-packages__\" ||\n pathname === \"/__widget-packages__/\"\n ) {\n await sendWidgetPackages(server, request, response);\n return;\n }\n\n next();\n });\n },\n };\n}\n\n/**\n * Widget projects intentionally have no HTML entrypoint, so Vite cannot\n * discover the worker's dependency graph during its normal startup scan.\n * Without an explicit entry, the first live-preview request discovers and\n * optimizes the portal worker after the server reports ready. Vite reloads\n * the worker mid-mount, closing its message thread and stranding the preview\n * in a permanent error state.\n *\n * Pre-optimizing the worker entry makes the initial graph stable before a\n * preview can connect. Vite follows this entry's imports, so React and the\n * Remote DOM worker runtime are optimized in the same startup pass.\n */\nexport function createWidgetDevServerConfig(): UserConfig {\n return {\n optimizeDeps: {\n include: [\"@fluid-app/portal-sdk/widgets/worker\"],\n },\n };\n}\n\n/** Source-relative path of a widget project's entry module. The dev\n * scaffold always places it here (see the widget template) — this is\n * the one thing the bootstrap script below needs from the plugin\n * that isn't itself part of the race/preamble fix. */\nconst WIDGET_ENTRY_MODULE_PATH = \"/src/index.ts\";\n\n/**\n * Builds the `/__runtime-entry__` worker bootstrap. Runtime-verified\n * against a real widget dev server + `@fluid-app/widget-runtime` host\n * in a Playwright harness (styled Leaderboard widget rendering,\n * mutation records flowing, `mounted.ready` resolving) — every piece\n * below exists to fix something that reproduced in that harness:\n *\n * 1. **Message-buffer race (the silent-mount killer).** The host\n * (widget-runtime) posts the \"mount\" message immediately after\n * `new Worker(...)`. A production-built worker bundle evaluates\n * synchronously and never races that message. In dev, though, the\n * worker's module graph loads asynchronously over many HTTP\n * requests (this bootstrap script, then the entry module, then\n * everything it imports, each a separate fetch) — so the \"mount\"\n * message can be dispatched and consumed by the Worker's default\n * (listener-less) handling before the widget entry's own\n * `message` listener, wired up deep inside the awaited import\n * below, is ever attached. The message is dropped silently, with\n * no error anywhere — the old `void import(\"/src/index.ts\")` also\n * swallowed the import's own rejections, compounding the silence.\n * Traced live: host→worker \"mount\" posts, zero worker→host replies\n * until this buffer-and-replay was added; after it, mutation\n * records flow and `mounted.ready` resolves. Fix: buffer every\n * message from the instant the worker boots (before anything else\n * runs), then replay the buffer, in order, once the entry module\n * has finished importing and has had the chance to attach its\n * real listener.\n * 2. **React-refresh preamble.** `@vitejs/plugin-react` injects a\n * preamble into every dev-transformed `.tsx` module referencing\n * `window.$RefreshReg$` / `$RefreshSig$`. A Worker global scope has\n * no `window` and no preamble, so importing a react-based widget\n * entry throws `window is not defined`, and — once `window` is\n * aliased to `globalThis` — throws again on the bare\n * `$RefreshSig$` alias. Installing the canonical preamble\n * (`RefreshRuntime.injectIntoGlobalHook` + no-op `$RefreshReg$` /\n * `$RefreshSig$`) before the entry import fixes both. The\n * `window = globalThis` alias was checked against widget-runtime's\n * worker lockdown / scope detection (which is\n * `globalThis.postMessage`-based) — no conflict. `/@react-refresh`\n * is `@vitejs/plugin-react`'s stable virtual module id, but a\n * widget project isn't guaranteed to use the react plugin, so the\n * whole block is try/catch-guarded — a project with no\n * `/@react-refresh` module (no react plugin) boots straight\n * through to the entry import unaffected.\n * 3. **`await import`, not `void import`.** A genuine failure to\n * import the entry module (syntax error, missing dependency, …)\n * must reject the worker's module evaluation loudly, not vanish\n * the way a fire-and-forget `void import` did.\n * 4. **Absolute dev-server imports.** Mist and portal previews load the\n * worker from another origin, so the host wraps this entry in a blob URL.\n * Root-relative specifiers inside a blob module resolve against the host\n * page (or fail to resolve at all), not this Vite server. Anchoring both\n * imports to the request origin keeps the entire dev module graph on the\n * widget server.\n *\n * Dev-only: this only ever serves from `configureServer` (the\n * plugin's `apply: \"serve\"` gate), never a built/published package.\n */\nexport function buildRuntimeEntryBootstrapScript(\n entryPath: string,\n origin: string,\n): string {\n const refreshRuntimeUrl = new URL(\"/@react-refresh\", origin).toString();\n const entryUrl = new URL(entryPath, origin).toString();\n\n return [\n \"// Dev-only worker bootstrap — see buildRuntimeEntryBootstrapScript\",\n \"// in packages/cli/widget/src/vite/plugin.ts for why each piece\",\n \"// here exists (message-buffer race, react-refresh preamble,\",\n \"// await-not-void import).\",\n \"const __q = [];\",\n \"const __buf = (e) => { __q.push(e.data); };\",\n 'self.addEventListener(\"message\", __buf);',\n 'if (typeof window === \"undefined\") { globalThis.window = globalThis; }',\n \"try {\",\n ` const { default: RefreshRuntime } = await import(${JSON.stringify(refreshRuntimeUrl)});`,\n \" RefreshRuntime.injectIntoGlobalHook(window);\",\n \" window.$RefreshReg$ = () => {};\",\n \" window.$RefreshSig$ = () => (type) => type;\",\n \" window.__vite_plugin_react_preamble_installed__ = true;\",\n \"} catch {\",\n \" // No @vitejs/plugin-react in this widget project (no\",\n \" // /@react-refresh virtual module) — nothing to preinstall.\",\n \"}\",\n `await import(${JSON.stringify(entryUrl)});`,\n 'self.removeEventListener(\"message\", __buf);',\n \"for (const data of __q) {\",\n ' self.dispatchEvent(new MessageEvent(\"message\", { data }));',\n \"}\",\n \"\",\n ].join(\"\\n\");\n}\n\n/**\n * The shared development descriptor workflow appends Vite's `?direct` query\n * to CSS URLs so preview hosts receive real CSS instead of JS style modules.\n * Published descriptors use a separate build path and never receive it.\n */\nexport function createDevWidgetPackageDescriptors(\n configModule: Readonly<Record<string, unknown>>,\n origin: string,\n): readonly RemoteDomWidgetPackageDescriptor[] {\n return [\n createWidgetPackageDevelopmentDescriptor({\n sourceModule: configModule,\n owner: \"droplet\",\n origin,\n workerEntryPath: \"/__runtime-entry__\",\n defaultCssPath: \"/styles.css\",\n }),\n ];\n}\n\nasync function sendWidgetPackages(\n server: ViteDevServer,\n request: IncomingMessage,\n response: ServerResponse,\n): Promise<void> {\n if (handleOptions(request, response)) return;\n\n try {\n const configModule = (await server.ssrLoadModule(\n \"/fluid.widget.config.ts\",\n )) as Record<string, unknown>;\n sendJson(\n response,\n createDevWidgetPackageDescriptors(\n configModule,\n getRequestOrigin(request),\n ),\n );\n } catch (error) {\n server.config.logger.error(\n `[fluid] Failed to load widget packages: ${String(error)}`,\n );\n sendJson(response, { error: String(error) }, 500);\n }\n}\n\nfunction sendJavaScript(\n request: IncomingMessage,\n response: ServerResponse,\n source: string,\n): void {\n if (handleOptions(request, response)) return;\n setCorsHeaders(response);\n response.setHeader(\"Content-Type\", \"text/javascript\");\n response.statusCode = 200;\n response.end(source);\n}\n\nfunction sendJson(\n response: ServerResponse,\n body: unknown,\n statusCode = 200,\n): void {\n setCorsHeaders(response);\n response.setHeader(\"Content-Type\", \"application/json\");\n response.statusCode = statusCode;\n response.end(JSON.stringify(body, null, 2));\n}\n\nfunction handleOptions(\n request: IncomingMessage,\n response: ServerResponse,\n): boolean {\n if (request.method !== \"OPTIONS\") return false;\n setCorsHeaders(response);\n response.statusCode = 204;\n response.end();\n return true;\n}\n\nfunction setCorsHeaders(response: ServerResponse): void {\n response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n response.setHeader(\"Access-Control-Allow-Methods\", \"GET, HEAD, OPTIONS\");\n response.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization\",\n );\n}\n\nfunction getRequestOrigin(request: IncomingMessage): string {\n const host = readHeader(request.headers.host);\n if (!host) throw new Error(\"Widget dev request is missing the Host header.\");\n\n const forwardedProtocol = readHeader(request.headers[\"x-forwarded-proto\"]);\n const protocol = forwardedProtocol === \"https\" ? \"https\" : \"http\";\n return `${protocol}://${host}`;\n}\n\nfunction readHeader(\n value: string | readonly string[] | undefined,\n): string | undefined {\n const candidate = Array.isArray(value) ? value[0] : value;\n return typeof candidate === \"string\" && candidate.trim().length > 0\n ? candidate.trim()\n : undefined;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-widget",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Fluid CLI plugin for standalone widget package workflows",
5
5
  "files": [
6
6
  "dist",
@@ -28,10 +28,12 @@
28
28
  "chalk": "^5.3.0",
29
29
  "commander": "^12.0.0",
30
30
  "execa": "^8.0.0",
31
+ "fs-extra": "^11.2.0",
31
32
  "ora": "^8.0.0",
32
33
  "prompts": "^2.4.2",
33
- "@fluid-app/fluid-cli": "0.1.14",
34
- "@fluid-app/fluid-cli-portal": "0.1.52"
34
+ "tsx": "^4.21.0",
35
+ "vite": "^6.4.1",
36
+ "@fluid-app/fluid-cli": "0.1.14"
35
37
  },
36
38
  "devDependencies": {
37
39
  "@types/node": "24.10.12",
@@ -39,8 +41,9 @@
39
41
  "@vitejs/plugin-react": "^4.3.0",
40
42
  "tsdown": "^0.21.0",
41
43
  "typescript": "^5",
42
- "vite": "^6.4.1",
43
44
  "vitest": "^4.0.18",
45
+ "@fluid-app/fluid-cli-widget-tooling": "0.1.0",
46
+ "@fluid-app/fluidos-api-client": "0.1.0",
44
47
  "@fluid-app/typescript-config": "0.0.0"
45
48
  },
46
49
  "engines": {
@@ -5,3 +5,9 @@ dist
5
5
  .env.local
6
6
  .fluid
7
7
  *.tsbuildinfo
8
+
9
+ # Local tooling state — never belongs in the widget repo.
10
+ # .mist-desktop/ holds Mist Desktop's private chats, attachments, and
11
+ # screenshots for this project; .mist is its clone/slug marker.
12
+ .mist
13
+ .mist-desktop/
@@ -18,6 +18,7 @@
18
18
  "@tanstack/react-query": "^5.90.11",
19
19
  "lucide-react": "~0.575.0",
20
20
  "react": "^19.2.4",
21
+ "react-dom": "^19.2.4",
21
22
  "react-hook-form": "^7.64.0",
22
23
  "zod": "4.3.5",
23
24
  "zustand": "^5.0.9"
@@ -29,7 +30,8 @@
29
30
  "vite": "^6.4.1",
30
31
  "@fluid-app/fluid-cli-widget": "{{widgetCliVersion}}",
31
32
  "@types/node": "^24.0.0",
32
- "@types/react": "^19.2.13"
33
+ "@types/react": "^19.2.13",
34
+ "@types/react-dom": "^19.2.3"
33
35
  },
34
36
  "engines": {
35
37
  "node": ">=24.0.0"