diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/dashboards/[dashboardId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/dashboards/[dashboardId]/page-client.tsx index 333fc15fac..dfcc3f76b8 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/dashboards/[dashboardId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/dashboards/[dashboardId]/page-client.tsx @@ -1,7 +1,8 @@ "use client"; -import { DashboardSandboxHost, type DashboardRuntimeError, type WidgetSelection } from "@/components/commands/create-dashboard/dashboard-sandbox-host"; +import { DashboardSandboxHost, stampEsmVersion, type DashboardRuntimeError, type WidgetSelection } from "@/components/commands/create-dashboard/dashboard-sandbox-host"; +import packageJson from "../../../../../../../../package.json"; import { useRouter, useRouterConfirm } from "@/components/router"; import { StreamingCodeViewer } from "@/components/streaming-code-viewer"; import { ActionDialog, Button, Typography, useToast } from "@/components/ui"; @@ -239,8 +240,9 @@ function DashboardDetailContent({ const handleCodeUpdate = useCallback((toolCall: ToolCallContent) => { if (typeof toolCall.args.content === "string") { - setPendingCode(toolCall.args.content); - setCurrentTsxSource(toolCall.args.content); + const stamped = stampEsmVersion(toolCall.args.content, packageJson.version); + setPendingCode(stamped); + setCurrentTsxSource(stamped); clearTimeout(codePhaseTimerRef.current); setCodePhase("typing"); codePhaseTimerRef.current = setTimeout(() => { @@ -450,7 +452,7 @@ function DashboardDetailContent({ } + toolComponents={ setCurrentTsxSource(stampEsmVersion(code, packageJson.version))} currentCode={currentTsxSource} />} useOffWhiteLightMode composerPlaceholder={currentHasSource ? undefined : DASHBOARD_COMPOSER_PLACEHOLDER} runningStatusMessages={!isCreating ? UPDATE_STATUS_MESSAGES : undefined} diff --git a/apps/dashboard/src/components/commands/create-dashboard/create-dashboard-preview.tsx b/apps/dashboard/src/components/commands/create-dashboard/create-dashboard-preview.tsx index 6691a3bf4e..aa75b67d12 100644 --- a/apps/dashboard/src/components/commands/create-dashboard/create-dashboard-preview.tsx +++ b/apps/dashboard/src/components/commands/create-dashboard/create-dashboard-preview.tsx @@ -21,8 +21,9 @@ import { useChat, type UIMessage } from "@ai-sdk/react"; import { convertToModelMessages, DefaultChatTransport } from "ai"; import { memo, useCallback, useMemo, useRef, useState } from "react"; import { CmdKPreviewProps } from "../../cmdk-commands"; -import { DashboardSandboxHost } from "./dashboard-sandbox-host"; +import { DashboardSandboxHost, stampEsmVersion } from "./dashboard-sandbox-host"; import { StreamingCodeViewer } from "../../streaming-code-viewer"; +import packageJson from "../../../../package.json"; type DashboardArtifact = { prompt: string, @@ -176,18 +177,19 @@ const CreateDashboardPreviewInner = memo(function CreateDashboardPreviewInner({ phase = "idle"; } - const displayCode = toolPart?.code ?? ""; + const displayCode = toolPart?.code ? stampEsmVersion(toolPart.code, packageJson.version) : ""; if (toolPart?.state === "input-available" && !artifact && !finalizedRef.current) { finalizedRef.current = true; const sanitized = sanitizeGeneratedCode(toolPart.code); + const stamped = stampEsmVersion(sanitized, packageJson.version); setArtifact({ prompt, projectId, runtimeCodegen: { title: prompt.slice(0, 120), description: "", - uiRuntimeSourceCode: sanitized, + uiRuntimeSourceCode: stamped, }, }); setIframeReady(false); diff --git a/apps/dashboard/src/components/commands/create-dashboard/dashboard-sandbox-host.tsx b/apps/dashboard/src/components/commands/create-dashboard/dashboard-sandbox-host.tsx index 30f5b5f90a..f0b9cee996 100644 --- a/apps/dashboard/src/components/commands/create-dashboard/dashboard-sandbox-host.tsx +++ b/apps/dashboard/src/components/commands/create-dashboard/dashboard-sandbox-host.tsx @@ -18,13 +18,57 @@ type DashboardArtifact = { function html(strings: TemplateStringsArray, ...values: unknown[]): string { return strings.reduce((result, str, i) => result + str + (values[i] ?? ''), ''); } - const isDev = process.env.NODE_ENV === "development"; +function getEsmFallbackVersion(version: string): string { + const parts = version.split("."); + if (parts.length !== 3) return version; + const patch = Number(parts[2]); + if (!Number.isInteger(patch) || patch <= 0) return version; + return `${parts[0]}.${parts[1]}.${patch - 1}`; +} + +const ESM_VERSION_HEADER = "// @stack-esm-version:"; +const ESM_VERSION_REGEX = /^\/\/\s*@stack-esm-version:\s*(\S+)\s*$/m; + +function extractEsmVersion(sourceCode: string): string | null { + const match = sourceCode.match(ESM_VERSION_REGEX); + return match ? match[1] : null; +} + +export function stampEsmVersion(sourceCode: string, version: string): string { + if (ESM_VERSION_REGEX.test(sourceCode)) { + return sourceCode.replace(ESM_VERSION_REGEX, `${ESM_VERSION_HEADER} ${version}`); + } + return `${ESM_VERSION_HEADER} ${version}\n${sourceCode}`; +} + function getDependencyScripts(esmVersion: string, esmFallbackVersion: string, dashboardUrl: string): string { if (isDev) { return html` `; @@ -72,6 +114,30 @@ function getDependencyScripts(esmVersion: string, esmFallbackVersion: string, da return html` `; } @@ -117,8 +189,8 @@ function escapeScriptContent(code: string): string { function getSandboxDocument(artifact: DashboardArtifact, baseUrl: string, dashboardUrl: string, initialTheme: "light" | "dark", showControls: boolean, initialChatOpen: boolean): string { const sourceCode = escapeScriptContent(artifact.runtimeCodegen.uiRuntimeSourceCode); const darkClass = initialTheme === "dark" ? "dark" : ""; - const esmVersion = packageJson.version; - const esmFallbackVersion = "2.8.71"; + const esmVersion = extractEsmVersion(artifact.runtimeCodegen.uiRuntimeSourceCode) ?? packageJson.version; + const esmFallbackVersion = getEsmFallbackVersion(esmVersion); const devScriptSrc = isDev ? ` ${dashboardUrl}` : ''; const devConnectSrc = isDev ? ` ${dashboardUrl}` : ''; @@ -307,10 +379,18 @@ function getSandboxDocument(artifact: DashboardArtifact, baseUrl: string, dashbo }; async function waitForDeps() { - if (window.__depsReady) return; - await new Promise(resolve => { - window.addEventListener('deps-ready', resolve, { once: true }); - }); + if (!window.__depsReady) { + await new Promise(resolve => { + window.addEventListener('deps-ready', resolve, { once: true }); + }); + } + if (window.__depsError) { + const error = new Error(window.__depsError.message || 'There was a problem loading custom dashboards. Please refresh the page and try again.'); + if (window.__depsError.stack) { + error.stack = window.__depsError.stack; + } + throw error; + } } async function requestAccessToken() { @@ -735,6 +815,13 @@ export const DashboardSandboxHost = memo(function DashboardSandboxHost({ return; } + if (type === "dashboard-sandbox-dependency-error") { + const err = new Error(event.data.message ?? 'Unknown custom dashboard dependency error'); + if (event.data.stack) err.stack = event.data.stack; + captureError('dashboard-sandbox-dependency-error', err); + return; + } + if (type === "dashboard-error-boundary") { const err = new Error(event.data.message ?? 'Unknown dashboard error'); if (event.data.stack) err.stack = event.data.stack;