From 4b23988958874378f6fa1a2b4aa6df081cd812a7 Mon Sep 17 00:00:00 2001 From: asemabdallah Date: Sun, 11 Oct 2026 02:28:20 +0200 Subject: [PATCH] fix(app): keep exported widget scripts separate --- .../__tests__/export-utils.test.ts | 27 +++++++++++++++++++ .../components/generative-ui/export-utils.ts | 11 ++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/app/src/components/generative-ui/__tests__/export-utils.test.ts b/apps/app/src/components/generative-ui/__tests__/export-utils.test.ts index 8853eee..196430d 100644 --- a/apps/app/src/components/generative-ui/__tests__/export-utils.test.ts +++ b/apps/app/src/components/generative-ui/__tests__/export-utils.test.ts @@ -88,6 +88,33 @@ describe("assembleStandaloneHtmlFromActivity", () => { expect(doc).toContain('document.title = "<\\/script>";'); }); + it("keeps js pieces separate when they omit trailing semicolons", async () => { + const doc = assembleStandaloneHtmlFromActivity({ + jsFunctions: "let count = 0", + jsExpressions: [ + "window.exportFirst = count + 1", + "(function () { window.exportSecond = 2 })()", + "[3].forEach(function (n) { window.exportThird = n })", + ], + }); + const script = doc.slice( + doc.lastIndexOf("") + ); + const globals = window as unknown as Record; + try { + expect(() => new Function(script)()).not.toThrow(); + await Promise.resolve(); + expect(globals.exportFirst).toBe(1); + expect(globals.exportSecond).toBe(2); + expect(globals.exportThird).toBe(3); + } finally { + delete globals.exportFirst; + delete globals.exportSecond; + delete globals.exportThird; + } + }); + it("neutralizes closing style sequences embedded in generated css", () => { const doc = assembleStandaloneHtmlFromActivity( { diff --git a/apps/app/src/components/generative-ui/export-utils.ts b/apps/app/src/components/generative-ui/export-utils.ts index 106eec4..fd80fac 100644 --- a/apps/app/src/components/generative-ui/export-utils.ts +++ b/apps/app/src/components/generative-ui/export-utils.ts @@ -25,6 +25,13 @@ export interface StandaloneActivityContent { const WEBSANDBOX_STUB = `window.Websandbox = { connection: { remote: { sendPrompt: async () => {}, openLink: async ({ url }) => { if (/^https:/.test(url)) window.open(url, "_blank", "noopener,noreferrer"); } } } };`; +// The live sandbox runs jsFunctions and each jsExpression as separate scripts, +// so generated code often omits trailing semicolons. Joined into one script, +// automatic semicolon insertion would merge the pieces: "let n = 0" followed +// by "(function () {...})()" parses as a call on 0. An explicit separator keeps +// each piece a separate statement. +const STATEMENT_SEPARATOR = "\n;\n"; + function escapeScriptClose(js: string): string { return js.replace(/<\/script/gi, "<\\/script"); } @@ -56,7 +63,7 @@ export function assembleStandaloneHtmlFromActivity( ...(expressions.length > 0 ? [ `(async () => { -${expressions.map(escapeScriptClose).join("\n")} +${expressions.map(escapeScriptClose).join(STATEMENT_SEPARATOR)} })();`, ] : []), @@ -64,7 +71,7 @@ ${expressions.map(escapeScriptClose).join("\n")} const generatedScript = scriptParts.length > 0 ? `` : ""; return `