From dc5420b64fed7f40b2948fe51bdf5fc8633b78c4 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 28 Jul 2026 14:13:47 -0700 Subject: [PATCH] fix: compile-check svelte variants at publish time Field failure (Codex session, 2026-07-28): the agent kept the seeded stub style block and appended its own second top-level style element in all three variants. Svelte forbids that, so the user saw a red Vite compile overlay; the mount-ack loop then self-healed (failure event, repair, republish, clean accept), but the overlay window is exactly the kind of thing the user should never see. The publish gate closes the class: a done reply for a component session now compile-checks every variant with the app's own compiler BEFORE the revision bump and the browser broadcast. Failures bounce as a 422 with file, line, and message plus _instructions; live-poll surfaces the details in the thrown reply error. The browser never imports a variant that cannot compile. Also: the stub guard comments warn that all CSS belongs in the single existing style block, worded to never contain the literal " --- skill/scripts/live-poll.mjs | 7 ++- skill/scripts/live-server.mjs | 16 +++++ skill/scripts/live/instructions.mjs | 2 +- skill/scripts/live/svelte-component.mjs | 43 ++++++++++++- tests/live-svelte-component-accept.test.mjs | 68 +++++++++++++++++++++ 5 files changed, 131 insertions(+), 5 deletions(-) diff --git a/skill/scripts/live-poll.mjs b/skill/scripts/live-poll.mjs index 32e872a66..3b2f08c9f 100644 --- a/skill/scripts/live-poll.mjs +++ b/skill/scripts/live-poll.mjs @@ -119,8 +119,11 @@ export async function postReply(base, token, reply) { }); if (!res.ok) { const body = await res.json().catch(() => ({})); - const parts = [body.error || res.statusText, body.reason, body.hint].filter(Boolean); - throw new Error(parts.join(': ')); + const failureLines = Array.isArray(body.failures) + ? body.failures.map((f) => ` ${f.file}${f.line != null ? `:${f.line}` : ''} ${f.message}`).join('\n') + : null; + const parts = [body.error || res.statusText, body.reason, body.hint, failureLines, body._instructions].filter(Boolean); + throw new Error(parts.join('\n')); } } diff --git a/skill/scripts/live-server.mjs b/skill/scripts/live-server.mjs index 29cb32a4b..62929ed90 100644 --- a/skill/scripts/live-server.mjs +++ b/skill/scripts/live-server.mjs @@ -55,6 +55,7 @@ import { import { applyDeferredSvelteComponentAccepts, bumpSvelteComponentPreviewRevision, + compileCheckVariants, removeAllSvelteComponentSessions, sweepInactiveSvelteComponentSessions, } from './live/svelte-component.mjs'; @@ -1322,9 +1323,24 @@ function handlePollPost(req, res) { // variant files into a fresh revision dir before the browser is told: // the import path changes every publish, so no transform cache can pin a // stale compile of a republished module (node_modules is unwatched). + // Broken variants are bounced HERE, before the browser imports anything: + // a compile error that reaches the page is a red overlay in the user's + // face; bounced at publish it is a private fix with file and line. if (replyFileMeta.previewMode === 'svelte-component' && msg.id && (msg.type === 'done' || !msg.type)) { + let compileCheck = { ok: true, failures: [] }; + try { compileCheck = compileCheckVariants(msg.id, process.cwd()); } catch { /* best-effort */ } + if (!compileCheck.ok) { + res.writeHead(422, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + error: 'variant_compile_failed', + id: msg.id, + failures: compileCheck.failures, + _instructions: 'The publish was NOT delivered: the listed variant file(s) do not compile, so the browser never saw them. Fix each failure at the given file and line (the most common cause is a second top-level \n` - : `\n\n`; + ? `\n\n` + : `\n\n`; return `${buildPropsScriptV2(contract)}${propsComment}${markupWithProps.trim()}\n${css}`; } @@ -1042,6 +1046,41 @@ export function removeSvelteComponentSession(id, cwd = process.cwd()) { } catch { /* non-fatal */ } } +/** + * Compile-check every variant component of a session with the app's own + * compiler, BEFORE the browser ever imports them. A variant that does not + * compile (the classic: a second top-level + + +`); + const broken = compileCheckVariants('gate0001', tmp3); + assert.equal(broken.ok, false); + assert.equal(broken.checked, 1); + assert.match(broken.failures[0].message, /single top-level/); + assert.match(broken.failures[0].file, /gate0001\/v1\.svelte/); + assert.equal(typeof broken.failures[0].line, 'number'); + + // Merged into one block: the gate opens. + write(tmp3, join(session.componentDir, 'v1.svelte'), ` + +
hi
+ + +`); + const fixed = compileCheckVariants('gate0001', tmp3); + assert.equal(fixed.ok, true, JSON.stringify(fixed.failures)); + } finally { + rmSync(tmp3, { recursive: true, force: true }); + } + }); +});