From 0520f41778b8b4d8c835bfd4f64fa6dcca784e9c Mon Sep 17 00:00:00 2001 From: Maksym Yezhov Date: Wed, 3 Jun 2026 10:28:30 -0700 Subject: [PATCH] fix: v2 - tangent - cors worker load --- .../components/AiChat/editorAgentWorker.ts | 11 +++-- .../RunView/toolBridge/runViewAgentWorker.ts | 11 +++-- src/utils/createCrossOriginWorker.ts | 40 +++++++++++++++++++ vite.config.js | 14 +++++++ 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/utils/createCrossOriginWorker.ts diff --git a/src/routes/v2/pages/Editor/components/AiChat/editorAgentWorker.ts b/src/routes/v2/pages/Editor/components/AiChat/editorAgentWorker.ts index 58a0123e7..428a60f20 100644 --- a/src/routes/v2/pages/Editor/components/AiChat/editorAgentWorker.ts +++ b/src/routes/v2/pages/Editor/components/AiChat/editorAgentWorker.ts @@ -1,11 +1,16 @@ /** * Worker factory for the Editor AI assistant. * - * The Editor window owns spawning its agent worker so the URL literal - * stays statically analyzable for Vite's worker bundling. + * The Editor window owns spawning its agent worker. We import the worker + * via `?worker&url` so Vite still runs it through the worker build pipeline + * (applying the `worker.plugins` shims), then hand the URL to + * `createCrossOriginWorker` which tolerates CDN-hosted (cross-origin) scripts. */ +import editorWorkerUrl from "@/agent/editorWorker.ts?worker&url"; +import { createCrossOriginWorker } from "@/utils/createCrossOriginWorker"; + export function createEditorAgentWorker(): Worker { - return new Worker(new URL("@/agent/editorWorker.ts", import.meta.url), { + return createCrossOriginWorker(editorWorkerUrl, { type: "module", name: "tangle-editor-agent", }); diff --git a/src/routes/v2/pages/RunView/toolBridge/runViewAgentWorker.ts b/src/routes/v2/pages/RunView/toolBridge/runViewAgentWorker.ts index e400593ea..849505d87 100644 --- a/src/routes/v2/pages/RunView/toolBridge/runViewAgentWorker.ts +++ b/src/routes/v2/pages/RunView/toolBridge/runViewAgentWorker.ts @@ -1,11 +1,16 @@ /** * Worker factory for the Run View AI assistant. * - * The Run View window owns spawning its agent worker so the URL literal - * stays statically analyzable for Vite's worker bundling. + * The Run View window owns spawning its agent worker. We import the worker + * via `?worker&url` so Vite still runs it through the worker build pipeline + * (applying the `worker.plugins` shims), then hand the URL to + * `createCrossOriginWorker` which tolerates CDN-hosted (cross-origin) scripts. */ +import runViewWorkerUrl from "@/agent/runViewWorker.ts?worker&url"; +import { createCrossOriginWorker } from "@/utils/createCrossOriginWorker"; + export function createRunViewAgentWorker(): Worker { - return new Worker(new URL("@/agent/runViewWorker.ts", import.meta.url), { + return createCrossOriginWorker(runViewWorkerUrl, { type: "module", name: "tangle-run-view-agent", }); diff --git a/src/utils/createCrossOriginWorker.ts b/src/utils/createCrossOriginWorker.ts new file mode 100644 index 000000000..6de01e3a7 --- /dev/null +++ b/src/utils/createCrossOriginWorker.ts @@ -0,0 +1,40 @@ +/** + * Constructs a module Web Worker that works even when the worker script is + * served from a different origin than the page (e.g. JS bundles on a CDN + * while the document is on the app origin). + * + * Browsers fetch the top-level worker script in `same-origin` mode, so a + * cross-origin script URL is rejected with a `DOMException` thrown + * synchronously inside `new Worker`. The fix is to build the worker from a + * same-origin `blob:` URL whose only content is an ESM `import` of the real + * (cross-origin) worker script. The blob inherits the page origin, satisfying + * the same-origin check, and the cross-origin module import is permitted by + * CORS. The worker's own relative imports keep resolving against the CDN. + * + * See https://github.com/vitejs/vite/issues/12662 and + * https://github.com/vitejs/vite/issues/13680. + */ +export function createCrossOriginWorker( + workerUrl: string, + options?: WorkerOptions, +): Worker { + const url = new URL(workerUrl, import.meta.url); + const workerOptions: WorkerOptions = { type: "module", ...options }; + + if (url.origin === self.location.origin) { + return new Worker(url, workerOptions); + } + + const source = `import ${JSON.stringify(url.href)};`; + const blob = new Blob([source], { type: "text/javascript" }); + const blobUrl = URL.createObjectURL(blob); + const worker = new Worker(blobUrl, workerOptions); + + // The blob only needs to live until the worker has fetched it. Revoke on + // error immediately, and best-effort after construction, to avoid leaking + // object URLs. + worker.addEventListener("error", () => URL.revokeObjectURL(blobUrl)); + setTimeout(() => URL.revokeObjectURL(blobUrl), 0); + + return worker; +} diff --git a/vite.config.js b/vite.config.js index 90434dc2a..339d4a8ff 100644 --- a/vite.config.js +++ b/vite.config.js @@ -71,6 +71,20 @@ export default defineConfig(({ mode }) => { : []), ], base: "/", + experimental: { + // Deploy base is unknown at build time: production serves assets from + // the origin root, tophat from a CDN subpath. Emit in-bundle asset + // URLs (notably the `?worker&url` scripts) as `import.meta.url`-relative + // so they resolve against the chunk location instead of an absolute + // `/assets/...` path that drops the tophat prefix. See + // `src/utils/createCrossOriginWorker.ts`. + renderBuiltUrl(filename, { hostType }) { + if (hostType === "js") { + return { relative: true }; + } + return { relative: false }; + }, + }, build: { manifest: "assets-registry.json", sourcemap: "hidden",