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

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/index.mjs CHANGED
@@ -1239,7 +1239,7 @@ const cloneCommand = new Command("clone").description("Clone a widget package's
1239
1239
  //#region src/utils/template.ts
1240
1240
  const EMBEDDED_PORTAL_SDK_VERSION = "0.1.461";
1241
1241
  const EMBEDDED_CLI_CORE_VERSION = "0.1.14";
1242
- const EMBEDDED_CLI_WIDGET_VERSION = "0.1.26";
1242
+ const EMBEDDED_CLI_WIDGET_VERSION = "0.1.27";
1243
1243
  function getDefaultTemplatePath() {
1244
1244
  return path.join(findPackageRoot(), "templates", "default");
1245
1245
  }
@@ -3,7 +3,60 @@ import { Plugin } from "vite";
3
3
 
4
4
  //#region src/vite/plugin.d.ts
5
5
  declare function fluidWidgetDevPlugin(): Plugin;
6
+ /**
7
+ * Builds the `/__runtime-entry__` worker bootstrap. Runtime-verified
8
+ * against a real widget dev server + `@fluid-app/widget-runtime` host
9
+ * in a Playwright harness (styled Leaderboard widget rendering,
10
+ * mutation records flowing, `mounted.ready` resolving) — every piece
11
+ * below exists to fix something that reproduced in that harness:
12
+ *
13
+ * 1. **Message-buffer race (the silent-mount killer).** The host
14
+ * (widget-runtime) posts the "mount" message immediately after
15
+ * `new Worker(...)`. A production-built worker bundle evaluates
16
+ * synchronously and never races that message. In dev, though, the
17
+ * worker's module graph loads asynchronously over many HTTP
18
+ * requests (this bootstrap script, then the entry module, then
19
+ * everything it imports, each a separate fetch) — so the "mount"
20
+ * message can be dispatched and consumed by the Worker's default
21
+ * (listener-less) handling before the widget entry's own
22
+ * `message` listener, wired up deep inside the awaited import
23
+ * below, is ever attached. The message is dropped silently, with
24
+ * no error anywhere — the old `void import("/src/index.ts")` also
25
+ * swallowed the import's own rejections, compounding the silence.
26
+ * Traced live: host→worker "mount" posts, zero worker→host replies
27
+ * until this buffer-and-replay was added; after it, mutation
28
+ * records flow and `mounted.ready` resolves. Fix: buffer every
29
+ * message from the instant the worker boots (before anything else
30
+ * runs), then replay the buffer, in order, once the entry module
31
+ * has finished importing and has had the chance to attach its
32
+ * real listener.
33
+ * 2. **React-refresh preamble.** `@vitejs/plugin-react` injects a
34
+ * preamble into every dev-transformed `.tsx` module referencing
35
+ * `window.$RefreshReg$` / `$RefreshSig$`. A Worker global scope has
36
+ * no `window` and no preamble, so importing a react-based widget
37
+ * entry throws `window is not defined`, and — once `window` is
38
+ * aliased to `globalThis` — throws again on the bare
39
+ * `$RefreshSig$` alias. Installing the canonical preamble
40
+ * (`RefreshRuntime.injectIntoGlobalHook` + no-op `$RefreshReg$` /
41
+ * `$RefreshSig$`) before the entry import fixes both. The
42
+ * `window = globalThis` alias was checked against widget-runtime's
43
+ * worker lockdown / scope detection (which is
44
+ * `globalThis.postMessage`-based) — no conflict. `/@react-refresh`
45
+ * is `@vitejs/plugin-react`'s stable virtual module id, but a
46
+ * widget project isn't guaranteed to use the react plugin, so the
47
+ * whole block is try/catch-guarded — a project with no
48
+ * `/@react-refresh` module (no react plugin) boots straight
49
+ * through to the entry import unaffected.
50
+ * 3. **`await import`, not `void import`.** A genuine failure to
51
+ * import the entry module (syntax error, missing dependency, …)
52
+ * must reject the worker's module evaluation loudly, not vanish
53
+ * the way a fire-and-forget `void import` did.
54
+ *
55
+ * Dev-only: this only ever serves from `configureServer` (the
56
+ * plugin's `apply: "serve"` gate), never a built/published package.
57
+ */
58
+ declare function buildRuntimeEntryBootstrapScript(entryPath: string): string;
6
59
  declare function createDevWidgetPackageDescriptors(configModule: Readonly<Record<string, unknown>>, origin: string): readonly RemoteDomWidgetPackageDescriptor[];
7
60
  //#endregion
8
- export { createDevWidgetPackageDescriptors, fluidWidgetDevPlugin };
61
+ export { buildRuntimeEntryBootstrapScript, createDevWidgetPackageDescriptors, fluidWidgetDevPlugin };
9
62
  //# sourceMappingURL=plugin.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.mts","names":[],"sources":["../../src/vite/plugin.ts"],"mappings":";;;;iBAQgB,oBAAA,CAAA,GAAwB,MAAA;AAAA,iBA8BxB,iCAAA,CACd,YAAA,EAAc,QAAA,CAAS,MAAA,oBACvB,MAAA,oBACU,gCAAA"}
1
+ {"version":3,"file":"plugin.d.mts","names":[],"sources":["../../src/vite/plugin.ts"],"mappings":";;;;iBAQgB,oBAAA,CAAA,GAAwB,MAAA;;AAAxC;;;;;AA4FA;;;;;AA6BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA7BgB,gCAAA,CAAiC,SAAA;AAAA,iBA6BjC,iCAAA,CACd,YAAA,EAAc,QAAA,CAAS,MAAA,oBACvB,MAAA,oBACU,gCAAA"}
@@ -8,7 +8,7 @@ function fluidWidgetDevPlugin() {
8
8
  server.middlewares.use(async (request, response, next) => {
9
9
  const pathname = (request.url ?? "").split("?")[0] ?? "";
10
10
  if (pathname === "/__runtime-entry__" || pathname === "/__runtime-entry__/") {
11
- sendJavaScript(request, response, "void import(\"/src/index.ts\");\n");
11
+ sendJavaScript(request, response, buildRuntimeEntryBootstrapScript(WIDGET_ENTRY_MODULE_PATH));
12
12
  return;
13
13
  }
14
14
  if (pathname === "/__widget-packages__" || pathname === "/__widget-packages__/") {
@@ -20,6 +20,91 @@ function fluidWidgetDevPlugin() {
20
20
  }
21
21
  };
22
22
  }
23
+ /** Source-relative path of a widget project's entry module. The dev
24
+ * scaffold always places it here (see the widget template) — this is
25
+ * the one thing the bootstrap script below needs from the plugin
26
+ * that isn't itself part of the race/preamble fix. */
27
+ const WIDGET_ENTRY_MODULE_PATH = "/src/index.ts";
28
+ /**
29
+ * Builds the `/__runtime-entry__` worker bootstrap. Runtime-verified
30
+ * against a real widget dev server + `@fluid-app/widget-runtime` host
31
+ * in a Playwright harness (styled Leaderboard widget rendering,
32
+ * mutation records flowing, `mounted.ready` resolving) — every piece
33
+ * below exists to fix something that reproduced in that harness:
34
+ *
35
+ * 1. **Message-buffer race (the silent-mount killer).** The host
36
+ * (widget-runtime) posts the "mount" message immediately after
37
+ * `new Worker(...)`. A production-built worker bundle evaluates
38
+ * synchronously and never races that message. In dev, though, the
39
+ * worker's module graph loads asynchronously over many HTTP
40
+ * requests (this bootstrap script, then the entry module, then
41
+ * everything it imports, each a separate fetch) — so the "mount"
42
+ * message can be dispatched and consumed by the Worker's default
43
+ * (listener-less) handling before the widget entry's own
44
+ * `message` listener, wired up deep inside the awaited import
45
+ * below, is ever attached. The message is dropped silently, with
46
+ * no error anywhere — the old `void import("/src/index.ts")` also
47
+ * swallowed the import's own rejections, compounding the silence.
48
+ * Traced live: host→worker "mount" posts, zero worker→host replies
49
+ * until this buffer-and-replay was added; after it, mutation
50
+ * records flow and `mounted.ready` resolves. Fix: buffer every
51
+ * message from the instant the worker boots (before anything else
52
+ * runs), then replay the buffer, in order, once the entry module
53
+ * has finished importing and has had the chance to attach its
54
+ * real listener.
55
+ * 2. **React-refresh preamble.** `@vitejs/plugin-react` injects a
56
+ * preamble into every dev-transformed `.tsx` module referencing
57
+ * `window.$RefreshReg$` / `$RefreshSig$`. A Worker global scope has
58
+ * no `window` and no preamble, so importing a react-based widget
59
+ * entry throws `window is not defined`, and — once `window` is
60
+ * aliased to `globalThis` — throws again on the bare
61
+ * `$RefreshSig$` alias. Installing the canonical preamble
62
+ * (`RefreshRuntime.injectIntoGlobalHook` + no-op `$RefreshReg$` /
63
+ * `$RefreshSig$`) before the entry import fixes both. The
64
+ * `window = globalThis` alias was checked against widget-runtime's
65
+ * worker lockdown / scope detection (which is
66
+ * `globalThis.postMessage`-based) — no conflict. `/@react-refresh`
67
+ * is `@vitejs/plugin-react`'s stable virtual module id, but a
68
+ * widget project isn't guaranteed to use the react plugin, so the
69
+ * whole block is try/catch-guarded — a project with no
70
+ * `/@react-refresh` module (no react plugin) boots straight
71
+ * through to the entry import unaffected.
72
+ * 3. **`await import`, not `void import`.** A genuine failure to
73
+ * import the entry module (syntax error, missing dependency, …)
74
+ * must reject the worker's module evaluation loudly, not vanish
75
+ * the way a fire-and-forget `void import` did.
76
+ *
77
+ * Dev-only: this only ever serves from `configureServer` (the
78
+ * plugin's `apply: "serve"` gate), never a built/published package.
79
+ */
80
+ function buildRuntimeEntryBootstrapScript(entryPath) {
81
+ return [
82
+ "// Dev-only worker bootstrap — see buildRuntimeEntryBootstrapScript",
83
+ "// in packages/cli/widget/src/vite/plugin.ts for why each piece",
84
+ "// here exists (message-buffer race, react-refresh preamble,",
85
+ "// await-not-void import).",
86
+ "const __q = [];",
87
+ "const __buf = (e) => { __q.push(e.data); };",
88
+ "self.addEventListener(\"message\", __buf);",
89
+ "if (typeof window === \"undefined\") { globalThis.window = globalThis; }",
90
+ "try {",
91
+ " const { default: RefreshRuntime } = await import(\"/@react-refresh\");",
92
+ " RefreshRuntime.injectIntoGlobalHook(window);",
93
+ " window.$RefreshReg$ = () => {};",
94
+ " window.$RefreshSig$ = () => (type) => type;",
95
+ " window.__vite_plugin_react_preamble_installed__ = true;",
96
+ "} catch {",
97
+ " // No @vitejs/plugin-react in this widget project (no",
98
+ " // /@react-refresh virtual module) — nothing to preinstall.",
99
+ "}",
100
+ `await import(${JSON.stringify(entryPath)});`,
101
+ "self.removeEventListener(\"message\", __buf);",
102
+ "for (const data of __q) {",
103
+ " self.dispatchEvent(new MessageEvent(\"message\", { data }));",
104
+ "}",
105
+ ""
106
+ ].join("\n");
107
+ }
23
108
  function createDevWidgetPackageDescriptors(configModule, origin) {
24
109
  const validation = validateSingleSourceWidgetPackage(readSourcePackages(configModule), { owner: "droplet" });
25
110
  if (!validation.success || !validation.value) throw new Error(validation.errors.map((error) => error.message).join("\n") || "Widget package validation failed.");
@@ -34,11 +119,32 @@ function validatedPackageToDescriptor(validated, origin) {
34
119
  source: validated.owner,
35
120
  version: validated.version,
36
121
  workerEntryUrl: new URL("/__runtime-entry__", origin).href,
37
- cssUrls: cssUrls.length > 0 ? cssUrls.map((url) => new URL(url, origin).href) : [new URL("/styles.css", origin).href],
122
+ cssUrls: cssUrls.length > 0 ? cssUrls.map((url) => toDevCssUrl(url, origin)) : [toDevCssUrl("/styles.css", origin)],
38
123
  assetUrls: [],
39
124
  widgets: validated.widgets.map(({ name: _name, ...widget }) => widget)
40
125
  };
41
126
  }
127
+ /**
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.
143
+ */
144
+ function toDevCssUrl(url, origin) {
145
+ const { href } = new URL(url, origin);
146
+ return href.includes("?") ? `${href}&direct` : `${href}?direct`;
147
+ }
42
148
  async function sendWidgetPackages(server, request, response) {
43
149
  if (handleOptions(request, response)) return;
44
150
  try {
@@ -89,6 +195,6 @@ function readHeader(value) {
89
195
  return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : void 0;
90
196
  }
91
197
  //#endregion
92
- export { createDevWidgetPackageDescriptors, fluidWidgetDevPlugin };
198
+ export { buildRuntimeEntryBootstrapScript, createDevWidgetPackageDescriptors, fluidWidgetDevPlugin };
93
199
 
94
200
  //# 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(request, response, 'void import(\"/src/index.ts\");\\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\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) => new URL(url, origin).href)\n : [new URL(\"/styles.css\", origin).href],\n assetUrls: [],\n widgets: validated.widgets.map(({ name: _name, ...widget }) => widget),\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 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 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-widget",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "Fluid CLI plugin for standalone widget package workflows",
5
5
  "files": [
6
6
  "dist",
@@ -30,8 +30,8 @@
30
30
  "execa": "^8.0.0",
31
31
  "ora": "^8.0.0",
32
32
  "prompts": "^2.4.2",
33
- "@fluid-app/fluid-cli-portal": "0.1.52",
34
- "@fluid-app/fluid-cli": "0.1.14"
33
+ "@fluid-app/fluid-cli": "0.1.14",
34
+ "@fluid-app/fluid-cli-portal": "0.1.52"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "24.10.12",