fix(exporters): compile JSX before export (N0) + semantic export E2E (N1) - #42
Open
stephschofield wants to merge 3 commits into
Open
stephschofield wants to merge 3 commits into
stephschofield wants to merge 3 commits into
Conversation
…factSource
Every export format shipped uncompiled JSX. `store.ts:exportActive` read
`previewHtml` — which is not HTML, but the agent's raw JSX module source —
and passed it straight to the exporters as `htmlContent`. `ensureDocumentShell`
saw no doctype, wrapped the bare JSX in a <body>, and wrote a file containing
literal JSX text with no React and no Babel. It opened to a blank page. All
five formats (html/pdf/pptx/zip/markdown) inherited this.
The three other consumers of the artifact (PreviewPane, done-verify,
DesignCardPreview) all compile via `buildSrcdoc` first. Export was the only
path that did not.
Root cause was naming: nothing in the type system said whether `htmlContent`
wanted JSX or HTML. This makes the contract checkable rather than implicit:
- Add `ArtifactSource = {kind:'jsx'|'html'; source:string}` and
`requireRenderedHtml()`, which throws instead of exporting raw JSX.
- Rename htmlContent -> artifactSource (IPC/preload) and -> renderedHtml
(exporter internals) end-to-end.
- Compile once via `buildSrcdoc` in `exportActive` before dispatch.
- Guard at both boundaries: `parseRequest` rejects kind:'jsx', and
`exportArtifact` re-checks at dispatch.
- Narrow `ExportRequest.artifactSource` to kind:'html' so "already compiled"
is a type-level guarantee, not a convention.
- Add EXPORTER_COMPILE_FAILED + en/pt-BR/zh-CN strings; surface compile
failures as that code rather than a generic export-failed toast.
Two further bugs found while writing the tests:
- `buildHtmlDocument` prepends its banner comment before the doctype, so
`ensureDocumentShell` failed to recognise its own output and wrapped a
second doctype around it on any re-export. Fixed by sniffing past leading
comments.
- Markdown export of a client-rendered document produced a 34-byte file
containing only YAML frontmatter — `convertBody` strips <head> and every
<script>, leaving nothing. It reported success. Now fails loudly; static
prerendering is N6.
Tests: html.ts had zero coverage, which is how this shipped unnoticed.
Adds html.test.ts (26 cases: document-shell characterization, the JSX
regression net, comment/idempotency edge cases) and export-pipeline.test.ts
(the compile-then-export green path, which lives in apps/desktop because
packages/exporters deliberately does not depend on @atv-design/runtime).
2280 tests pass; typecheck and lint clean.
…parse N1. The originally-specified assertion for this test was "the exported file has an HTML doctype". That assertion is vacuous — `buildHtmlDocument` prepends a doctype unconditionally, so it passed on the pre-N0 output: a file of literal, unexecutable JSX that opened to a blank page. The test as specified would have certified the bug it existed to catch. This asserts the property a user actually cares about: the file renders. It drives the real export IPC (the same path the Export menu uses, with only the save dialog stubbed), opens the written file in an Electron BrowserWindow with all network egress blocked at the session level, and requires that `#root` gained children and the artifact's own content is on screen. Network blocking is load-bearing. `buildSrcdoc` inlines React, ReactDOM, and Babel, so a correct export renders fully offline; if a refactor swaps those inlines for CDN tags, this fails. Verified by mutation: reverting the compile step in `exportActive` to the pre-N0 behaviour makes this test fail on the `#root` wait (30s timeout, no children — the blank page), and restoring it makes it pass. A regression test that cannot catch the regression is worse than none. The run surfaced two genuinely blocked requests: the Tailwind CDN tag and the hardcoded Google Fonts stylesheet. Both are known (plan A5: `en.json:629` already claims HTML export is "single self-contained" and is false today). Resolving them is N6, so they are pinned explicitly with a message telling the next person to tighten the bound to zero once N6 lands, rather than hidden behind a blanket filter. Playwright's Electron driver cannot create browser contexts (`Target.createTarget` unsupported), hence the BrowserWindow + `webRequest` approach rather than `page.route`.
…acefully Unblocks the manual export verification in PR #42, and fixes two real bugs found while doing it. 1. PDF and PPTX export were broken in every packaged build. `pnpm --prod deploy` prunes by apps/desktop/package.json. `puppeteer-core` and `pptxgenjs` were only reachable through `@atv-design/exporters`, which is a devDependency, so neither ever made it into the shipped asar — confirmed by listing app.asar: 0 entries for both. Exporting to PDF or PPTX from an installed build failed at runtime with ERR_MODULE_NOT_FOUND. `zip-lib` survived purely because it happened to be listed as a direct dep already. Both are now declared alongside it, and a test asserts all three stay there (verified to fail when one is removed). This never showed up in dev because the lazy imports resolve fine against the workspace node_modules. 2. `pnpm build` died on any host without rpmbuild. electron-builder shells out to external tools for Linux installers — rpm needs `rpmbuild`, deb needs `dpkg` — and neither is bundled. On a host missing one, the build failed after compiling, rebuilding native modules, and downloading Electron, taking all of `pnpm build` down with it. The failure is environmental, not a defect, but it blocked every local task needing a runnable binary. Default builds now drop only the targets the host genuinely cannot build and log why. AppImage has no external dependency so it always survives. An explicit `--linux <target>` or `--dir` is left alone, so asking for an unbuildable target is still a loud error rather than a silent downgrade. CI images have both tools, so release builds are unaffected. Note the target list is passed as `--linux a b`, NOT repeated `--config.linux.target=` flags — that key is a scalar, so each occurrence overwrote the last and only the final target survived. The first cut of this silently dropped the AppImage; there is a regression test pinning it. 3. Adds e2e/export-all-formats.spec.ts. Drives the real export IPC for all five formats and checks each written file for format-appropriate evidence (PDF/PPTX/ZIP magic bytes, compiled runtime in the HTML). Result against a packaged build: html 3317715 bytes compiled runtime present pdf 37595 bytes valid pdf container pptx 47518 bytes valid pptx container zip 719159 bytes valid zip container markdown 0 bytes correctly refused (client-rendered; N6 will fix) Markdown refusing is the intended N0 behaviour, not a gap: the compiled artifact renders on the client, so there is no static HTML left to convert. A second test exports a static HTML artifact to markdown successfully, proving the guard rejects client-rendered documents specifically rather than disabling markdown export wholesale.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Every export format shipped uncompiled JSX.
store.ts:exportActivereadpreviewHtml— which is not HTML, but the agent's raw JSX module source — and handed it to the exporters ashtmlContent.ensureDocumentShellsaw no doctype, wrapped the bare JSX in a<body>, and wrote a file containing literal JSX text with no React and no Babel. It opened to a blank page. All five formats (html/pdf/pptx/zip/markdown) inherited this.The three other consumers of the artifact —
PreviewPane,done-verify,DesignCardPreview— all compile viabuildSrcdocfirst. Export was the only path that did not.Root cause was naming: nothing in the type system said whether
htmlContentwanted JSX or HTML.N0 — make the contract checkable
ArtifactSource = {kind:'jsx'|'html'; source:string}+requireRenderedHtml(), which throws rather than exporting raw JSX.htmlContent→artifactSource(IPC/preload) and →renderedHtml(exporter internals), end to end.buildSrcdocinexportActive, before dispatch.parseRequestrejectskind:'jsx';exportArtifactre-checks at dispatch.ExportRequest.artifactSourcenarrowed tokind:'html', so "already compiled" is a type-level guarantee rather than a convention.EXPORTER_COMPILE_FAILEDcode + en/pt-BR/zh-CN strings; compile failures surface as that code instead of a generic export-failed toast.Two further bugs found while writing the tests
buildHtmlDocumentprepends its banner comment before the doctype, soensureDocumentShellfailed to recognise its own output and wrapped a second doctype on any re-export. Fixed by sniffing past leading comments.convertBodystrips<head>and every<script>, leaving nothing) — and reported success. Now fails loudly. Static prerendering is N6.N1 — an E2E that can actually catch this
The originally-specified assertion was "the exported file has an HTML doctype." That is vacuous: the doctype is prepended unconditionally, so it passed on the broken output. The test as specified would have certified the bug it existed to catch.
This asserts the property a user cares about: the file renders. It drives the real export IPC (only the save dialog is stubbed), opens the written file in an Electron
BrowserWindowwith all network egress blocked at the session level, and requires#rootgained children and the artifact's content is on screen.Verified by mutation: reverting the compile step makes this test fail on the
#rootwait (30s timeout, no children — the blank page); restoring it makes it pass. A regression test that cannot catch the regression is worse than none.The run surfaced two genuinely blocked requests — the Tailwind CDN tag and the hardcoded Google Fonts stylesheet. Both are known (plan A5:
en.json:629already claims HTML export is "single self-contained", which is false today). Resolving them is N6, so they are pinned explicitly with a message telling the next person to tighten the bound to zero once N6 lands, rather than hidden behind a blanket filter.Test plan
pnpm test— 2292 passing, 0 failingpnpm typecheck— 10/10 packagespnpm lint— cleanpnpm build— now completes (AppImage + deb; rpm skipped with a clear message)html.tshad zero coverage — which is how this shipped unnoticed. Addshtml.test.ts(26 cases) +export-pipeline.test.ts(green path; lives inapps/desktopbecausepackages/exportersdeliberately does not depend on@atv-design/runtime)Automated as
e2e/export-all-formats.spec.ts, which drives the real export IPC (only the save dialog is stubbed) and checks each written file for format-appropriate evidence — magic bytes for the PDF/PPTX/ZIP containers, compiled runtime for the HTML.Markdown refusing is the intended N0 behaviour, not a gap: the compiled artifact renders on the client, so no static HTML survives
convertBodyto convert. A companion test exports a static HTML artifact to markdown successfully, proving the guard rejects client-rendered documents specifically rather than disabling markdown export wholesale.Getting there surfaced two more real bugs
PDF and PPTX export were broken in every packaged build.
pnpm --prod deployprunes byapps/desktop/package.json, andpuppeteer-core/pptxgenjswere only reachable through@atv-design/exporters— a devDependency. Listingapp.asarconfirmed 0 entries for both, so exporting to PDF or PPTX from an installed build failed withERR_MODULE_NOT_FOUND.zip-libsurvived only by luck: it was already a direct dep. Never visible in dev, where the lazy imports resolve against the workspacenode_modules. Both are now declared, with a test pinning all three (verified to fail when one is removed).pnpm builddied on any host withoutrpmbuild. electron-builder shells out to external tools for Linux installers (rpm→rpmbuild, deb→dpkg) and bundles neither, so the build failed after compiling, rebuilding native modules, and downloading Electron — taking all ofpnpm buildwith it. Default builds now drop only what the host genuinely can't build and log why; AppImage always survives; an explicit--linux <target>/--diris still a loud error rather than a silent downgrade. CI images have both tools, so release builds are unaffected.Pre-existing, not from this PR
6
design-system-*E2E screenshot-baseline failures. Confirmed identical on base commitbebb02d; those specs contain zero references to export.Follow-ups (already scoped in the plan)