fix(plugin): prevent theme flash in embedded workbenches

This commit is contained in:
t8y2 committed 2026-09-14 14:43:02 +08:00
1 parent 8d32cf9ffa
commit f1cc68a4ba
4 files changed
+49 -4

No files matched your search

+12
View File
@@ -297,6 +297,18 @@ describe("PluginHostBridge", () => {
expect(document).toContain("var(--color-background");
});
it("bootstraps the initial theme before the plugin document paints", () => {
const document = pluginSandboxDocument("<html><head></head><body></body></html>", [], {
appearance: "dark",
tokens: { "--color-background": "rgb(19 20 22)", "--color-destructive": "rgb(243 98 95)" },
});
expect(document).toContain('root.dataset.dbxTheme = theme.appearance === "dark" ? "dark" : "light"');
expect(document).toContain('root.style.colorScheme = theme.appearance === "dark" ? "dark" : "light"');
expect(document).toContain('"--color-background":"rgb(19 20 22)"');
expect(document).toContain('"--color-destructive":"rgb(243 98 95)"');
});
it("forwards the plugin close-tab shortcut to the host", () => {
const target = { postMessage: vi.fn() } as unknown as Window;
const closeTab = vi.fn();
@@ -217,17 +217,24 @@ export function pluginNetworkOrigins(permissions: readonly string[] | undefined)
return [...origins];
}
export function pluginSandboxDocument(html: string, permissions?: readonly string[]): string {
export function pluginSandboxDocument(html: string, permissions?: readonly string[], theme?: PluginBridgeTheme): string {
const networkOrigins = pluginNetworkOrigins(permissions);
const connectSrc = networkOrigins.length > 0 ? `connect-src ${networkOrigins.join(" ")};` : "connect-src 'none';";
const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline' blob:; img-src data: blob:; font-src data: blob:; ${connectSrc} media-src data: blob:;">`;
const sdk = `<script>${pluginSdkSource()}</script>`;
const uiKit = `<style>${pluginUiKitCss()}</style>`;
const injection = `${csp}${uiKit}${sdk}`;
const themeBootstrap = pluginThemeBootstrap(theme);
const injection = `${csp}${uiKit}${themeBootstrap}${sdk}`;
if (/<head(?:\s[^>]*)?>/i.test(html)) return html.replace(/<head(?:\s[^>]*)?>/i, (head) => `${head}${injection}`);
return `<!doctype html><html><head>${injection}</head><body>${html}</body></html>`;
}
function pluginThemeBootstrap(theme?: PluginBridgeTheme): string {
if (!theme) return "";
const serializedTheme = JSON.stringify(theme).replace(/</g, "\\u003c");
return `<script>(() => { const theme = ${serializedTheme}; const root = document.documentElement; root.dataset.dbxTheme = theme.appearance === "dark" ? "dark" : "light"; root.style.colorScheme = theme.appearance === "dark" ? "dark" : "light"; for (const [name, value] of Object.entries(theme.tokens || {})) { if (/^--[a-z0-9-]+$/i.test(name) && typeof value === "string") root.style.setProperty(name, value); } })();</script>`;
}
/**
* Minimal official component kit for plugin workbenches. Every class is built
* on the DBX design tokens the host pushes through the bridge, so plugin UI