Skip to content

feat(exporters): real PDF/PPTX/ZIP via system Chrome + pptxgenjs + zip-lib (lazy-loaded)#7

Closed
hqhq1025 wants to merge 2 commits intomainfrom
wt/exporters
Closed

feat(exporters): real PDF/PPTX/ZIP via system Chrome + pptxgenjs + zip-lib (lazy-loaded)#7
hqhq1025 wants to merge 2 commits intomainfrom
wt/exporters

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Replaces the EXPORTER_NOT_READY stubs in packages/exporters/src/{pdf,pptx,zip}.ts with three lazy-loaded tier-1 implementations. Decisions locked in docs/research/04-pptx-export.md.

Stacks on top of #4 (wt/first-demo). Rebase or land that PR first; this branch contains the same commits plus one new commit on top.

Why each format works the way it does

PDF — puppeteer-core + system Chrome

  • We refuse to bundle Chromium. PRINCIPLES §1 caps the installer at 80 MB; a Chromium download is ~150 MB.
  • chrome-discovery.ts walks canonical paths per OS (Mac .app, Win %ProgramFiles%, Linux which) plus a CODESIGN_CHROME_PATH override.
  • Throws CodesignError('System Chrome not found, install from https://www.google.com/chrome', 'EXPORTER_NO_CHROME') when nothing resolves — no silent fallback (PRINCIPLES §10).
  • Tier 1: no header/footer, no font embedding, default Letter page format (auto available for tall single-sheet output).

PPTX — pptxgenjs only

  • One slide per top-level <section>; whole document if no sections exist.
  • We deliberately skip dom-to-pptx in tier 1 — unmaintained (last release ages ago), only adds value for the small set of layouts already covered by our regex extractor. Tier 2 can revisit.
  • CJK fix: pptxgenjs defaults emit bodyPr wrap="square" and we explicitly set fit: 'shrink' (= normAutofit), sidestepping the dom-to-pptx CJK overflow bug from research/04 §"Known traps".

ZIP — zip-lib

  • ~80 KB, MIT, zero deps. Stable archive layout: index.html at root, README.md (generated banner with timestamp + restore instructions), optional assets/ subtree.
  • Staging directory pattern so Zip.addFile always points at a real file path (zip-lib's API takes disk paths, not buffers).

Files

New

  • packages/exporters/src/chrome-discovery.ts + .test.ts (6 cases: mac path, win ProgramFiles, linux which, override, missing, linux-missing)
  • packages/exporters/src/pdf.test.ts (3 cases — happy / override / wrapped failure, all puppeteer mocked)
  • packages/exporters/src/pptx.test.ts (6 cases incl. real CJK end-to-end write that produces a valid .pptx on disk)
  • packages/exporters/src/zip.test.ts (3 cases incl. multi-asset round-trip via Unzip)

Modified

  • packages/exporters/src/{pdf,pptx,zip}.ts — real impls
  • packages/exporters/src/index.tsisExporterReady flips to true; dispatch passes htmlContent + destinationPath
  • packages/exporters/package.json — three new prod deps
  • apps/desktop/src/main/exporter-ipc.ts — comment refresh; routing already supported all four formats
  • apps/desktop/src/renderer/src/components/PreviewToolbar.tsxready: true on PDF/PPTX/ZIP; tooltips describe what each format produces

New production dependencies

Package License Disk size (du -sh) Lazy-load proof
puppeteer-core@^24 Apache-2.0 12 MB await import('puppeteer-core') inside exportPdf body
pptxgenjs@^3.12 MIT 2.6 MB await import('pptxgenjs') inside exportPptx body
zip-lib@^1.0.4 MIT 100 KB await import('zip-lib') inside exportZip body
$ grep -nE "await import\('(puppeteer-core|pptxgenjs|zip-lib)'\)" packages/exporters/src/*.ts
packages/exporters/src/pdf.ts:31:  const puppeteer = (await import('puppeteer-core')).default;
packages/exporters/src/pptx.ts:32:  const PptxGenJS = (await import('pptxgenjs')).default;
packages/exporters/src/zip.ts:54:  const { Zip } = await import('zip-lib');

All three are runtime deps but live entirely outside the cold-start module graph — the renderer never touches them, and the main process only resolves them the first time the user clicks Export → {PDF|PPTX|ZIP}.

Install size

du -sh release/ was not measured because the desktop build pipeline is a follow-up (no electron-builder config landed yet in this worktree). The on-disk dep contribution is bounded above by du -sh node_modules/.pnpm/{puppeteer-core,pptxgenjs,zip-lib} = 14.7 MB unpacked. After Electron asar packing + tree-shaking on import-time-only modules, expect ≤ 6 MB shipped. Re-confirm in the install-size CI gate once it's wired.

Acceptance test outcomes

  1. PDF — pnpm dev → Export PDF → opens in Preview/Acrobat (manually verified locally on macOS with installed Chrome)
  2. PPTX — opens in Keynote and PowerPoint Mac with text editable; npm vitest run packages/exporters/src/pptx.test.ts confirms PK\x03\x04 magic bytes + bytes > 1 KB
  3. ZIP — Unzip round-trip in zip.test.ts confirms index.html, README.md, and assets/* are all present
  4. CJK — pptx.test.ts writes <section><h1>你好世界</h1><p>第一张幻灯片</p></section> to a real file; manual PowerPoint Mac open shows no overlap
  5. No-Chrome — chrome-discovery.test.ts "throws EXPORTER_NO_CHROME with install link" passes; loud CodesignError, no fallback

CI checklist

  • pnpm -r typecheck — green
  • pnpm lint — green (Biome, no warnings)
  • pnpm -r test — 18 new + all existing tests pass

§5b checks

  • Compatible: ExporterFormat enum unchanged, IPC payload unchanged, exportArtifact signature unchanged
  • Upgradeable: tier-1 boundaries preserved — extractSlides is the seam where dom-to-pptx will plug in; chrome-discovery's ChromeDiscoveryDeps is the seam for tests/electron-managed Chromium later
  • No bloat: 3 deps, 14.7 MB unpacked, all lazy-loaded; no stub libraries pulled in
  • Elegant: no off-the-shelf "AI app" affordances; toolbar copy describes the actual output of each format

Signed-off-by: Haoqing Wang 1506751656@qq.com

Wires the full prompt → artifact → preview → export loop so the README's
"first demo" actually produces a design when an API key is provided.

- packages/templates: externalize the design-generator system prompt as
  SYSTEM_PROMPTS.designGenerator with a sibling design-generator.md for
  reviewable diffs. Replaces the inline string previously hard-coded in
  packages/core. Prompt embeds the research-backed Claude Design rules
  (single artifact, Tailwind CDN, semantic HTML, CSS variable tokens,
  WCAG AA, no lorem ipsum).
- packages/core: pull SYSTEM_PROMPTS.designGenerator from templates,
  collapse the duplicated artifact-extraction loop into a `collect()`
  helper, add generate.test.ts (mocks the providers boundary, asserts
  empty-prompt error, artifact extraction shape, system-prompt wiring).
- packages/exporters: real exportHtml() that ensures a doctype, injects
  the Tailwind CDN tag if missing, stamps a generator meta/banner, and
  pretty-prints. PDF / PPTX / ZIP each throw CodesignError with code
  EXPORTER_NOT_READY ("ships in Phase 2") — no silent fallbacks
  (PRINCIPLES §10). Top-level exportArtifact() dispatches lazily so
  unused formats stay out of the cold-start bundle (PRINCIPLES §1).
- apps/desktop: codesign:export IPC backed by Electron dialog
  showSaveDialog, validates payload via CodesignError, propagates
  Phase-2 errors loudly. Preload exposes window.codesign.export().
  Store gains exportActive(format) + a toast slot. PreviewToolbar
  renders an Export dropdown with HTML enabled and PDF/PPTX/ZIP
  disabled with "Coming in Phase 2" tooltips.
- TIER 1 / dev-only fallback: store reads VITE_OPEN_CODESIGN_DEV_KEY
  so the demo runs before wt/onboarding lands real keychain plumbing.
  Marked clearly for removal in the integration commit.
- examples/calm-spaces: README documents the demo + expected behaviour
  + intentional loud failure modes.

No new third-party dependencies. All UI uses var(--color-*) tokens.

Acceptance test (manual):
  VITE_OPEN_CODESIGN_DEV_KEY=sk-ant-... \
    pnpm --filter @open-codesign/desktop dev
  → click "Calm Spaces meditation app" → Send → iframe renders
  → Export → HTML → /tmp/out.html → open in browser
  → Export → PDF → toast "PDF export ships in Phase 2"

Signed-off-by: Haoqing Wang <1506751656@qq.com>
…p-lib

Replaces the EXPORTER_NOT_READY stubs with three lazy-loaded tier-1
implementations decided in docs/research/04-pptx-export.md:

- PDF: puppeteer-core (~12 MB) drives the user's installed Chrome.
  We refuse to bundle Chromium (PRINCIPLES §1: 80 MB cap; Chromium
  alone is ~150 MB). chrome-discovery.ts walks the canonical paths
  per-OS plus a CODESIGN_CHROME_PATH override and throws
  EXPORTER_NO_CHROME with the install URL when nothing is found —
  no silent fallback (PRINCIPLES §10).

- PPTX: pptxgenjs (~2.6 MB). One slide per top-level <section>;
  whole document if none. Defaults emit bodyPr wrap=square and
  fit=shrink, sidestepping the dom-to-pptx CJK overflow bug
  (research/04 §"Known traps"). dom-to-pptx itself stays out of
  tier 1 — adding an unmaintained dep for the small subset of
  layouts it covers would violate "no bloat".

- ZIP: zip-lib (~100 KB, MIT, zero deps). Stable layout:
  index.html at root, README.md with a generated banner, optional
  assets/ subtree. No streaming-write hooks for tier 1.

All three exporter functions use `await import(...)` inside the
function body, so cold-start does not pull any of these into the
module graph. Verified via grep across packages/exporters/src/*.ts.

Toolbar + IPC handler updated to flip readiness on PDF/PPTX/ZIP and
swap the "Coming in Phase 2" tooltip for per-format hover-help.

Tests: 18 new vitest cases — chrome discovery for mac/win/linux
+ override + missing, mocked puppeteer happy/override/error paths,
PPTX extractSlides + a real CJK end-to-end write that PowerPoint
opens, ZIP multi-asset round-trip via Unzip.

Signed-off-by: hqhq1025 <1506751656@qq.com>
@hqhq1025
Copy link
Copy Markdown
Collaborator Author

Superseded by v2 (clean cherry-pick on top of latest main). See 7-v2 PR.

@hqhq1025 hqhq1025 closed this Apr 18, 2026
@hqhq1025 hqhq1025 deleted the wt/exporters branch April 18, 2026 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant