@fluid-app/fluid-cli-widget 0.1.27 → 0.1.29
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-JKSTx5pZ.d.mts +199 -0
- package/dist/index-JKSTx5pZ.d.mts.map +1 -0
- package/dist/index.d.mts +3 -15
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1304 -315
- package/dist/index.mjs.map +1 -1
- package/dist/src-_wfDoBLp.mjs +1997 -0
- package/dist/src-_wfDoBLp.mjs.map +1 -0
- package/dist/vite/plugin.d.mts +2 -62
- package/dist/vite/plugin.mjs +2 -200
- package/package.json +8 -5
- package/templates/default/.gitignore.template +6 -0
- package/templates/default/AGENTS.md +15 -0
- package/templates/default/package.json.template +3 -1
- package/templates/default/styles.css +47 -15
- package/dist/vite/plugin.d.mts.map +0 -1
- package/dist/vite/plugin.mjs.map +0 -1
package/dist/vite/plugin.mjs
CHANGED
|
@@ -1,200 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
function fluidWidgetDevPlugin() {
|
|
4
|
-
return {
|
|
5
|
-
name: "fluid-widget-dev-routes",
|
|
6
|
-
apply: "serve",
|
|
7
|
-
configureServer(server) {
|
|
8
|
-
server.middlewares.use(async (request, response, next) => {
|
|
9
|
-
const pathname = (request.url ?? "").split("?")[0] ?? "";
|
|
10
|
-
if (pathname === "/__runtime-entry__" || pathname === "/__runtime-entry__/") {
|
|
11
|
-
sendJavaScript(request, response, buildRuntimeEntryBootstrapScript(WIDGET_ENTRY_MODULE_PATH));
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
if (pathname === "/__widget-packages__" || pathname === "/__widget-packages__/") {
|
|
15
|
-
await sendWidgetPackages(server, request, response);
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
next();
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
};
|
|
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
|
-
}
|
|
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
|
-
/**
|
|
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
|
-
}
|
|
148
|
-
async function sendWidgetPackages(server, request, response) {
|
|
149
|
-
if (handleOptions(request, response)) return;
|
|
150
|
-
try {
|
|
151
|
-
sendJson(response, createDevWidgetPackageDescriptors(await server.ssrLoadModule("/fluid.widget.config.ts"), getRequestOrigin(request)));
|
|
152
|
-
} catch (error) {
|
|
153
|
-
server.config.logger.error(`[fluid] Failed to load widget packages: ${String(error)}`);
|
|
154
|
-
sendJson(response, { error: String(error) }, 500);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
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
|
-
function sendJavaScript(request, response, source) {
|
|
164
|
-
if (handleOptions(request, response)) return;
|
|
165
|
-
setCorsHeaders(response);
|
|
166
|
-
response.setHeader("Content-Type", "text/javascript");
|
|
167
|
-
response.statusCode = 200;
|
|
168
|
-
response.end(source);
|
|
169
|
-
}
|
|
170
|
-
function sendJson(response, body, statusCode = 200) {
|
|
171
|
-
setCorsHeaders(response);
|
|
172
|
-
response.setHeader("Content-Type", "application/json");
|
|
173
|
-
response.statusCode = statusCode;
|
|
174
|
-
response.end(JSON.stringify(body, null, 2));
|
|
175
|
-
}
|
|
176
|
-
function handleOptions(request, response) {
|
|
177
|
-
if (request.method !== "OPTIONS") return false;
|
|
178
|
-
setCorsHeaders(response);
|
|
179
|
-
response.statusCode = 204;
|
|
180
|
-
response.end();
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
function setCorsHeaders(response) {
|
|
184
|
-
response.setHeader("Access-Control-Allow-Origin", "*");
|
|
185
|
-
response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
|
186
|
-
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
187
|
-
}
|
|
188
|
-
function getRequestOrigin(request) {
|
|
189
|
-
const host = readHeader(request.headers.host);
|
|
190
|
-
if (!host) throw new Error("Widget dev request is missing the Host header.");
|
|
191
|
-
return `${readHeader(request.headers["x-forwarded-proto"]) === "https" ? "https" : "http"}://${host}`;
|
|
192
|
-
}
|
|
193
|
-
function readHeader(value) {
|
|
194
|
-
const candidate = Array.isArray(value) ? value[0] : value;
|
|
195
|
-
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : void 0;
|
|
196
|
-
}
|
|
197
|
-
//#endregion
|
|
198
|
-
export { buildRuntimeEntryBootstrapScript, createDevWidgetPackageDescriptors, fluidWidgetDevPlugin };
|
|
199
|
-
|
|
200
|
-
//# sourceMappingURL=plugin.mjs.map
|
|
1
|
+
import { a as buildRuntimeEntryBootstrapScript, c as fluidWidgetDevPlugin, o as createDevWidgetPackageDescriptors, s as createWidgetDevServerConfig } from "../src-_wfDoBLp.mjs";
|
|
2
|
+
export { buildRuntimeEntryBootstrapScript, createDevWidgetPackageDescriptors, createWidgetDevServerConfig, fluidWidgetDevPlugin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-widget",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
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
|
-
"
|
|
34
|
-
"
|
|
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,9 +41,10 @@
|
|
|
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",
|
|
44
|
-
"@fluid-app/
|
|
45
|
+
"@fluid-app/fluid-cli-widget-tooling": "0.1.0",
|
|
46
|
+
"@fluid-app/typescript-config": "0.0.0",
|
|
47
|
+
"@fluid-app/fluidos-api-client": "0.1.0"
|
|
45
48
|
},
|
|
46
49
|
"engines": {
|
|
47
50
|
"node": ">=24.0.0"
|
|
@@ -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/
|
|
@@ -210,6 +210,21 @@ Light/dark behavior:
|
|
|
210
210
|
|
|
211
211
|
Runtime CSS rules:
|
|
212
212
|
|
|
213
|
+
**A stylesheet is the only way to style a widget. Inline styles do not work.**
|
|
214
|
+
|
|
215
|
+
The host sanitizes every Remote DOM mutation before it reaches the shadow root. The
|
|
216
|
+
`style` attribute is rejected and `<style>` elements are a banned tag, so:
|
|
217
|
+
|
|
218
|
+
- `style={{ color: "red" }}` on any element is **silently dropped**. The element renders
|
|
219
|
+
with no styling and no error — this is the single most common reason a widget "looks
|
|
220
|
+
broken" or "came out unstyled".
|
|
221
|
+
- An inline `<style>` block never renders.
|
|
222
|
+
- Only CSS reachable from the widget build graph is published as a `cssUrls` artifact and
|
|
223
|
+
adopted into the shadow root.
|
|
224
|
+
|
|
225
|
+
So: put every rule in a stylesheet, give elements `className`s, and target those classes.
|
|
226
|
+
Never reach for an inline style — it will not survive.
|
|
227
|
+
|
|
213
228
|
- Put runtime selectors in `styles.css` or imported CSS modules.
|
|
214
229
|
- Keep selectors prefixed with the widget name to avoid leaking styles into the host.
|
|
215
230
|
- Do not rely on global body styles for runtime appearance.
|
|
@@ -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"
|
|
@@ -1,22 +1,47 @@
|
|
|
1
|
+
/* Runtime styles for the example widget.
|
|
2
|
+
*
|
|
3
|
+
* This file is the worked example every new widget is copied from, so it is
|
|
4
|
+
* deliberately written the way widget CSS SHOULD be written:
|
|
5
|
+
*
|
|
6
|
+
* • Colors, fonts, and radii come from the HOST's semantic CSS variables.
|
|
7
|
+
* The portal theme sets them, they inherit through the widget's shadow
|
|
8
|
+
* root, and the same widget then looks correct on every portal and in
|
|
9
|
+
* both light and dark — without a second, hand-written dark palette.
|
|
10
|
+
*
|
|
11
|
+
* • Every var() carries a fallback. Tokens are absent in some hosts (a bare
|
|
12
|
+
* dev harness, a preview surface that has not adopted the portal theme),
|
|
13
|
+
* and a widget must still be legible there rather than collapsing to
|
|
14
|
+
* unstyled text. The fallback is a floor, never the intended look.
|
|
15
|
+
*
|
|
16
|
+
* • Selectors stay prefixed with the widget name so nothing leaks into the
|
|
17
|
+
* host, and nothing here depends on global body styles.
|
|
18
|
+
*
|
|
19
|
+
* Do not reintroduce hard-coded brand colors. A widget that hard-codes
|
|
20
|
+
* #ffffff / #111827 ignores the portal's theme entirely and will look wrong
|
|
21
|
+
* on any portal that is not white-on-near-black. See AGENTS.md → "Theme and
|
|
22
|
+
* styling guidance". */
|
|
23
|
+
|
|
1
24
|
.review-carousel {
|
|
2
|
-
font-family:
|
|
25
|
+
font-family: var(
|
|
26
|
+
--font-body,
|
|
3
27
|
Inter,
|
|
4
28
|
ui-sans-serif,
|
|
5
29
|
system-ui,
|
|
6
30
|
-apple-system,
|
|
7
31
|
BlinkMacSystemFont,
|
|
8
32
|
"Segoe UI",
|
|
9
|
-
sans-serif
|
|
10
|
-
|
|
11
|
-
border
|
|
33
|
+
sans-serif
|
|
34
|
+
);
|
|
35
|
+
border: 1px solid var(--border, #e5e7eb);
|
|
36
|
+
border-radius: var(--radius-xl, 24px);
|
|
12
37
|
padding: 32px;
|
|
13
|
-
background: #ffffff;
|
|
38
|
+
background: var(--card, #ffffff);
|
|
14
39
|
box-shadow: 0 24px 80px rgb(15 23 42 / 10%);
|
|
15
40
|
}
|
|
16
41
|
|
|
17
42
|
.review-carousel__eyebrow {
|
|
18
43
|
margin: 0 0 8px;
|
|
19
|
-
color: #4f46e5;
|
|
44
|
+
color: var(--primary, #4f46e5);
|
|
20
45
|
font-size: 0.75rem;
|
|
21
46
|
font-weight: 700;
|
|
22
47
|
letter-spacing: 0.08em;
|
|
@@ -25,7 +50,8 @@
|
|
|
25
50
|
|
|
26
51
|
.review-carousel__title {
|
|
27
52
|
margin: 0;
|
|
28
|
-
color: #111827;
|
|
53
|
+
color: var(--card-foreground, #111827);
|
|
54
|
+
font-family: var(--font-header, inherit);
|
|
29
55
|
font-size: clamp(2rem, 4vw, 3.5rem);
|
|
30
56
|
line-height: 0.95;
|
|
31
57
|
letter-spacing: -0.05em;
|
|
@@ -33,7 +59,7 @@
|
|
|
33
59
|
|
|
34
60
|
.review-carousel__quote {
|
|
35
61
|
margin: 32px 0 0;
|
|
36
|
-
color: #1f2937;
|
|
62
|
+
color: var(--card-foreground, #1f2937);
|
|
37
63
|
font-size: clamp(1.25rem, 2vw, 1.75rem);
|
|
38
64
|
line-height: 1.35;
|
|
39
65
|
}
|
|
@@ -48,13 +74,13 @@
|
|
|
48
74
|
|
|
49
75
|
.review-carousel__author {
|
|
50
76
|
margin: 0;
|
|
51
|
-
color: #111827;
|
|
77
|
+
color: var(--card-foreground, #111827);
|
|
52
78
|
font-weight: 700;
|
|
53
79
|
}
|
|
54
80
|
|
|
55
81
|
.review-carousel__role {
|
|
56
82
|
margin: 4px 0 0;
|
|
57
|
-
color: #6b7280;
|
|
83
|
+
color: var(--muted-foreground, #6b7280);
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
.review-carousel__controls {
|
|
@@ -65,15 +91,21 @@
|
|
|
65
91
|
.review-carousel__button {
|
|
66
92
|
width: 40px;
|
|
67
93
|
height: 40px;
|
|
68
|
-
border: 1px solid #d1d5db;
|
|
94
|
+
border: 1px solid var(--border, #d1d5db);
|
|
95
|
+
/* Intentionally a pill, not themed — shape is this control's identity. */
|
|
69
96
|
border-radius: 999px;
|
|
70
|
-
color: #111827;
|
|
71
|
-
background: #ffffff;
|
|
97
|
+
color: var(--card-foreground, #111827);
|
|
98
|
+
background: var(--card, #ffffff);
|
|
72
99
|
font: inherit;
|
|
73
100
|
cursor: pointer;
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
.review-carousel__button:hover {
|
|
77
|
-
border-color: #4f46e5;
|
|
78
|
-
color: #4f46e5;
|
|
104
|
+
border-color: var(--primary, #4f46e5);
|
|
105
|
+
color: var(--primary, #4f46e5);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.review-carousel__button:focus-visible {
|
|
109
|
+
outline: 2px solid var(--ring, #4f46e5);
|
|
110
|
+
outline-offset: 2px;
|
|
79
111
|
}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/vite/plugin.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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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"}
|