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 }); + } + }); +});