From 1b24e503a1d366bc88cd126e40a6cf41116747aa Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 11 May 2026 10:41:32 -0500 Subject: [PATCH 001/108] docs(q2-preview): epic plan + beads (bd-kw93) for q2 preview workstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture: ephemeral local hub-client (samod + file watcher) serves a pared-down React SPA that renders via the q2-preview format. Engines run server-side via `engine: replay` and the EngineCapture flows through automerge to the browser, which replays in WASM. No DOM rebuild on edit — Bootstrap / MathJax / reveal.js state survives. Phasing: A (skeleton + standalone SPA serving), B (broadened watcher + dep-graph invalidation), C (record-on-demand engine + replay), D (polish), E (stretch). All seven open questions resolved in the 2026-05-11 reviews — see plan §Resolutions from 2026-05-11 review #2. Two follow-ups filed: - bd-hfjj (epic, blocks bd-kw93): hub-client decomposition — shared preview-pane package so hub-client and the preview SPA stay in lockstep by construction. - bd-56b0 (task, related to bd-kw93): cross-doc dependency channel audit. Phase A's always-visible force-refresh button is the user escape hatch until this lands. Branch: feature/q2-preview. No code changes; this is the plan + beads index update. --- .beads/issues.jsonl | 3 + .../plans/2026-05-11-q2-preview-epic.md | 766 ++++++++++++++++++ 2 files changed, 769 insertions(+) create mode 100644 claude-notes/plans/2026-05-11-q2-preview-epic.md diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 7d5eee462..e674b8496 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -74,6 +74,7 @@ {"id":"bd-4g6g","title":"[websites epic] Move sidebar to Q1 template position (sidebar-left)","description":"Phase 2 puts the sidebar beside the TOC on the right — minimum churn in the existing FULL_HTML_TEMPLATE. Q1 renders sidebar-left, TOC-right. Moving to the Q1 layout is a template restructuring task: change the two-column grid, adjust layout CSS, and re-verify any layout-sensitive tests. Separate from sidebar feature work.\n\nPlan reference: claude-notes/plans/2026-04-24-websites-phase-2.md Decision 4 + follow-up.","status":"open","priority":3,"issue_type":"task","created_at":"2026-04-24T17:53:05.037896Z","created_by":"cscheid","updated_at":"2026-04-24T17:53:05.037896Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-4g6g","depends_on_id":"bd-9svl","type":"discovered-from","created_at":"2026-04-24T17:53:05.037896Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-4ho9","title":"L9 follow-up: validate against W3C feed validator","description":"L9 v1 ships snapshot tests + manual end-to-end inspection. File any parse warnings raised by canonical RSS validators on representative outputs and fix. Validators to try: W3C feed validator (online), feedvalidator.org's offline tool, Pandoc's own RSS schema if available.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-08T17:33:22.925543Z","created_by":"cscheid","updated_at":"2026-05-08T17:33:22.925543Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-4ho9","depends_on_id":"bd-o90m","type":"discovered-from","created_at":"2026-05-08T17:33:22.925543Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-4zdf","title":"Draft-mode interaction with sitemap","description":"Phase 7 emits sitemap entries for every profiled page including drafts. When draft-mode YAML config lands (see bd-p4sc and friends from Phase 6), gate the sitemap emission so draft pages are omitted unless draft-mode == \"visible\". Originating phase: bd-b9mz; coordinate with bd-p4sc.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-04-27T15:03:22.529546Z","created_by":"cscheid","updated_at":"2026-04-27T15:03:22.529546Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-4zdf","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-27T15:03:22.529546Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-4zdf","depends_on_id":"bd-b9mz","type":"discovered-from","created_at":"2026-04-27T15:03:22.529546Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-4zdf","depends_on_id":"bd-p4sc","type":"related","created_at":"2026-04-27T15:03:22.529546Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-56b0","title":"Cross-doc dependency channel audit for q2 preview","description":"Enumerate every Quarto feature that creates a cross-document dependency the preview pipeline needs to react to: include shortcodes, listing content globs, bibliography/csl paths, theme SCSS imports, project-scoped resources, _extensions/ Lua filters, and anything else. For each: which DocumentProfile channel encodes it today (if any), which need to be added, which stay manual-refresh-only. Drives Phase B (channels already on DocumentProfile) and informs Phase D (new channels). Until this lands, the always-visible manual force-refresh button (Phase A.6) is the user escape hatch. See claude-notes/plans/2026-05-11-q2-preview-epic.md §Recommended next steps item 3 + Risk 2.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-11T15:40:54.896280Z","created_by":"cscheid","updated_at":"2026-05-11T15:40:54.896280Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-56b0","depends_on_id":"bd-kw93","type":"related","created_at":"2026-05-11T15:40:54.896280Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-57y4","title":"Vendor and integrate quarto-listing.scss with theme-CSS pipeline","description":"Per L3 D5 (decided 2026-05-06), the listing JS pair (list.min.js + quarto-listing.js) shipped via the artifact store as Project-scoped js: artifacts in the L3 phase 7 commit. The third asset, quarto-listing.scss, was deferred because it requires Bootstrap variable wiring + media-breakpoint mixins from the existing theme-CSS pipeline (CompileThemeCssStage / quarto_sass::SassLayer). That's a larger design task than the static JS bundling.\n\nThis issue: integrate the listing SCSS as a SassLayer that gets composed with the website's theme bundle. The Q1 source file lives at external-sources/quarto-cli/src/resources/projects/website/listing/quarto-listing.scss; copy to resources/listing/quarto-listing.scss per the External Sources Policy and add it to the theme bundle when at least one listing is rendered.\n\nWhile the SCSS isn't compiled, listings render with default browser styling — the markup and JS are unchanged. The list / grid / table layouts work but lack the polished card / table styling Q1 ships.\n\nDiscovered while shipping L3 phase 7 (bd-ml8z); see D5 in claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md.\n\nL5 (bd-5vsr) lands the markup that consumes this SCSS; merging bd-57y4 restores Q1 visual parity for category sidebars and per-item category chips.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-06T20:52:15.894830Z","created_by":"cscheid","updated_at":"2026-05-07T13:46:18.157025Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-57y4","depends_on_id":"bd-ml8z","type":"discovered-from","created_at":"2026-05-06T20:52:15.894830Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-5qnj","title":"Manage trace size for use as replay/regression-test fixtures","description":"Spun out of bd-45yw (replay engine). Once traces double as replay fixtures (and as user-attached bug-report artifacts), trace size becomes a real constraint — they will be checked into the repo as regression fixtures and posted by users in issues.\n\nCurrent quarto-trace output captures per-stage pipeline state (see crates/quarto-trace/src/lib.rs and crates/quarto-core/src/stage/trace.rs::JsonTraceObserver), which is already on the heavy side for routine diagnostic use.\n\nInvestigate:\n- Where the bulk lives in current traces (per-stage AST snapshots? supporting_files content? something else?)\n- Which content is actually load-bearing for replay vs. diagnostics, and whether the two roles can share one artifact (the design preference noted in bd-45yw's plan)\n- Compression on disk; lazy loading on read\n- Whether per-stage AST snapshots can be elided/diffed when not needed\n- Size budgets — what is reasonable for (a) checked-in CI fixtures and (b) user-attached bug reports\n\nGoal: make 'one trace serves both diagnostic and replay roles' practical. If size cannot be bounded, fall back to two separate artifacts and document why.\n\nReferences:\n- crates/quarto-trace/src/lib.rs (TraceDocument, TraceEntry)\n- crates/quarto-core/src/stage/trace.rs (JsonTraceObserver)\n- claude-notes/plans/2026-05-03-replay-engine.md (open subquestion in Phase 1)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-03T21:11:19.859036Z","created_by":"cscheid","updated_at":"2026-05-03T23:29:05.405253Z","closed_at":"2026-05-03T23:29:05.405068Z","close_reason":"Phases 1, 2, 3, and 5 complete. End-to-end verified on bd-5qnj fixtures: big.qmd 16.3 MB pretty -> 62 KB gzipped+deduped (~265x). Unified-artifact promise realized post-merge with bd-45yw. Real-engine supporting_files re-measurement tracked as bd-sr73.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5qnj","depends_on_id":"bd-45yw","type":"related","created_at":"2026-05-03T21:11:19.859036Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-5vsr","title":"L5 — Categories sidebar","description":"Right-margin category list emitted from L3's resolved item set, grouped by category. Three Q1-parity styles: category-default, category-unnumbered, category-cloud. Templates embedded as doctemplate via MemoryResolver. Click-to-filter interactivity scoped out of v1. See claude-notes/plans/2026-05-05-listings-epic.md §L5.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-05-05T19:53:32.333630Z","created_by":"cscheid","updated_at":"2026-05-07T16:00:41.250061Z","closed_at":"2026-05-07T16:00:41.249924Z","close_reason":"L5 — categories sidebar landed on feature/listings via merge 9e8afa0d (impl 2750546b + follow-on 67a985f4). Per-item chips + right-margin sidebar with three modes (default/unnumbered/cloud); Q-12-11 and Q-12-12 diagnostics; aggregate across multi-listing pages. End-to-end CLI verification recorded in plan §End-to-end CLI verification record. cargo xtask verify clean (Rust + hub-client + WASM). Test count 8570 → 8621 (+51). Discovered-from follow-ups filed: bd-99ru (localize labels), bd-754f (review encoding scheme), bd-ra5j (hub-client browser smoke deferred), bd-nwyp (PandocInlines parser audit).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5vsr","depends_on_id":"bd-61cd","type":"parent-child","created_at":"2026-05-05T19:53:32.333630Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-5vsr","depends_on_id":"bd-ml8z","type":"blocks","created_at":"2026-05-05T19:53:32.333630Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} @@ -164,6 +165,7 @@ {"id":"bd-h5l7","title":"Diagnose and fix SourceInfo::eq hotspot in hub-client preview","description":"Chrome profile of hub-client on a moderately-sized document shows a large hotspot on ::eq, entered from parse_qmd_to_ast (crates/wasm-quarto-hub-client/src/lib.rs:757). Likely cause: SourceInfoSerializer::intern in crates/pampa/src/writers/json.rs:229 linearly scans content_map (Vec<(SourceInfo, usize)>) on every pointer-lookup miss, and SourceInfo is held by-value in AST nodes so pointer lookups almost always miss — giving O(n²) behavior per document. Also the first perf-profiling session on Quarto 2, so the plan deliberately establishes a repeatable native-side workflow (Criterion bench + samply flamegraph on the pampa binary) before any fix, since Chrome-side before/after is too noisy to iterate on. Plan: claude-notes/plans/2026-04-22-sourceinfo-eq-hotspot.md. Currently in draft — awaiting user review before any measurement work begins.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-04-22T20:00:48.019984Z","created_by":"cscheid","updated_at":"2026-04-22T23:23:38.315697Z","closed_at":"2026-04-22T23:23:38.315082Z","close_reason":"Landed on perf/2026-04-22-json-sourcemap in commit 8f6f21d9. Browser cross-validation confirmed SourceInfo::eq no longer a hotspot.","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-h736","title":"Default project render regression and project-level diagnostics","description":"Post-websites-merge: presence of `_quarto.yml` with `project: { type: default }` causes `q2 render` to silently produce zero output, and `q2 render index.qmd` to fail with a misleading 'excluded from render list' error. Root cause: `default_output_dir` returns the project root for default projects, so the discovery walker's output_dir-exclusion check rejects every file. Three goals: (1) fix the discovery regression so default projects walk the tree like websites do; (2) add a clear project-level diagnostic when the render set is empty so we no longer silently no-op; (3) add a project-level diagnostic surface that both the CLI and hub-client can render, since today only file-level diagnostics flow to hub-client. Plan: claude-notes/plans/2026-05-01-default-project-render-diagnostics.md (open design questions inside, awaiting user input before implementation starts).","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-05-01T21:40:15.795872Z","created_by":"cscheid","updated_at":"2026-05-01T22:08:43.216214Z","closed_at":"2026-05-01T22:08:43.216070Z","close_reason":"Fixed default-project discovery regression (output_dir == project_dir) and added Q-PROJECT-EMPTY project-level diagnostic with non-zero exit. Plumbing reuses existing ProjectRenderSummary.project_diagnostics infrastructure, so hub-client surfaces the diagnostic via the existing warnings array. End-to-end verified against the user's fixture in claude-notes/plans/2026-05-01-default-project-render-diagnostics.md. Commits: dd959bdd (Phase 1: discovery fix + tests), fd06b8da (Phase 2: empty-set diagnostic + CLI exit policy).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-h736","depends_on_id":"bd-0tr6","type":"discovered-from","created_at":"2026-05-01T21:40:15.795872Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-hb8h","title":"Design: cargo-dependency-upgrade skill for periodic dependency maintenance","description":"Design and implement an on-demand /upgrade-cargo-deps skill for periodic (bi-weekly) Rust dependency maintenance. Design settled 2026-05-04: skill-only (no scheduling); applies patch/minor upgrades automatically in a worktree, surfaces major upgrades for human judgment via plan doc + per-major beads issues; runs full 'cargo xtask verify' after applying; uses only built-in cargo tooling (cargo update, cargo tree --duplicates); npm equivalent deferred to v2. See claude-notes/plans/2026-05-04-cargo-dependency-upgrade-skill.md for full design and work items.","status":"in_progress","priority":2,"issue_type":"task","created_at":"2026-05-04T15:49:18.455915Z","created_by":"cscheid","updated_at":"2026-05-04T16:19:43.493917Z","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-hfjj","title":"Hub-client decomposition: shared preview-pane package for hub-client + q2 preview SPA","description":"Design sprint + implementation of the React-component decomposition that lets hub-client's preview pane and the q2 preview SPA share components — same source files, same imports, same tests. Drawing the package boundary correctly is what makes new preview features land in both surfaces by construction (no second copy to maintain). Blocks Phase A of bd-kw93. See claude-notes/plans/2026-05-11-q2-preview-epic.md (§Build-time concerns, §Crate / SPA layout invariant, §Recommended next steps item 1). Open: exact package count + names (one shared package for render-time primitives? two for render + sync? more?), workspace layout, build-script wiring.","status":"open","priority":1,"issue_type":"epic","created_at":"2026-05-11T15:40:51.137344Z","created_by":"cscheid","updated_at":"2026-05-11T15:41:14.854956Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-ht0n","title":"[websites] Sidebar logo / subtitle / header / footer","description":"Render the sidebar's logo (with light/dark variants + logo-href + logo-alt), subtitle (parsed but not rendered in Phase 2), and the 'header:' / 'footer:' freeform content slots Q1 supports. All are parsed by Sidebar::from_config_value today but ignored in sidebar_to_html.","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-24T17:52:25.088205Z","created_by":"cscheid","updated_at":"2026-04-24T17:52:25.088205Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-ht0n","depends_on_id":"bd-9svl","type":"discovered-from","created_at":"2026-04-24T17:52:25.088205Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-hva0","title":"Local-vendor opt-in for MathJax/KaTeX (offline rendering)","description":"bd-w5ov shipped CDN-default for math (parity with Pandoc / Quarto 1). Users who need offline / air-gapped rendering must today set 'html-math-method.url:' pointed at a self-hosted mirror.\n\nMake this easier: ship a 'quarto install mathjax' (and 'quarto install katex') CLI helper that downloads the bundle into a project resources dir and writes the URL override into _quarto.yml. This is 'we're better than Q1' territory — Q1 offers no equivalent automation.\n\nOut of scope for this issue: vendoring bytes by default in the binary (rejected in bd-w5ov §4.5 because of binary-size cost and parity with Q1).\n\nAcceptance: 'quarto install mathjax' downloads, lays out under project resources, configures override; 'quarto render' uses the local URL; offline rendering works.","status":"open","priority":2,"issue_type":"feature","created_at":"2026-05-04T23:58:29.291389Z","created_by":"cscheid","updated_at":"2026-05-04T23:58:29.291389Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-hva0","depends_on_id":"bd-w5ov","type":"discovered-from","created_at":"2026-05-04T23:58:29.291389Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-hzsi","title":"L10 — Q1 → Q2 listing template migration docs + LLM skill","description":"User-facing migration doc in docs/ covering EJS → doctemplate mapping (<%= … %> → $…$, control flow, helper-function → server-pre-rendered fields, item.extra). LLM skill in .claude/skills/ that suggests Q2 doctemplate equivalents from Q1 EJS templates. Worked examples for each built-in shape and a representative custom template. See claude-notes/plans/2026-05-05-listings-epic.md §L10.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-05T19:53:59.836077Z","created_by":"cscheid","updated_at":"2026-05-05T19:53:59.836077Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-hzsi","depends_on_id":"bd-61cd","type":"parent-child","created_at":"2026-05-05T19:53:59.836077Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-hzsi","depends_on_id":"bd-rqgx","type":"blocks","created_at":"2026-05-05T19:53:59.836077Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} @@ -190,6 +192,7 @@ {"id":"bd-k9i1","title":"project.resources support for non-renderable site resources","description":"Q1 supports project.resources: [patterns...] in _quarto.yml, which are globs for files to copy to the output dir alongside rendered HTML but not passed through the render pipeline (e.g. CNAME, robots.txt, images not referenced from qmds). Phase 1 of the website epic does not support this. Implement in ProjectPipeline (probably a third phase after Pass 2) or WebsiteProjectType::post_render.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-04-24T01:05:17.897545Z","created_by":"cscheid","updated_at":"2026-04-24T01:05:17.897545Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-k9i1","depends_on_id":"bd-w5os","type":"discovered-from","created_at":"2026-04-24T01:05:17.897545Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-khuj","title":"Hub-client UI smoke for template diagnostics","description":"After bd-xdnk plumbed doctemplate diagnostics through quarto render, the hub-client side rides the existing diagnostics_to_json -> JsonDiagnostic.warnings rails. No code changes were needed there, but a live browser smoke test would confirm a Q-10-2 warning shows up in the hub-client diagnostics panel and as a Monaco marker on the template file.\n\nSteps:\n1. Open hub-client against a project containing a custom template that references an undefined variable.\n2. Confirm the warning appears in the diagnostics panel with the correct file/line/column.\n3. Confirm Monaco shows a marker on the template's variable position when that file is open.\n4. Capture screenshots or notes in the bd-xdnk plan.\n\nDiscovered while doing bd-xdnk; not blocking that fix because the data flow uses well-tested rails.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-05T16:36:08.563749Z","created_by":"cscheid","updated_at":"2026-05-05T16:36:08.563749Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-khuj","depends_on_id":"bd-xdnk","type":"discovered-from","created_at":"2026-05-05T16:36:08.563749Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-kk0a","title":"qmd writer: position-dependent escapes (`:`, `'`/`\"` unbalanced, `1.`/`-`/`+` line-start list markers)","description":"Discovered during the bd-21gu Phase 2 audit (claude-notes/plans/2026-04-30-at-escape-qmd-roundtrip.md). The qmd writer's escape_markdown helper is char-by-char and has no position context, so it can't fix escapes that depend on whether a char is at line-start or in unbalanced-quote position. The Phase 2 audit identified four bug classes that all need the same shape of fix:\n\n1. `:` at line start — triggers fenced div parser. Mid-line `:` is fine.\n Repro: a Str body containing \":word\" at the start of a line round-trips\n to a parse error.\n\n2. `'` and `\"` unbalanced — when a Str body coming from JSON contains a\n literal `'` or `\"` (not part of a Quoted node), the writer emits it\n unescaped and the re-parser produces an unclosed-quote error. Mid-word\n apostrophe (`a'b`) round-trips fine via smart-quote handling.\n\n3. `1.`/`-`/`+` followed by Space at line start — Str \"1.\" + Space + Str\n \"foo\" round-trips to OrderedList. Same for unordered list markers `-` /\n `+` followed by space at line start. The dot/dash itself is innocuous\n mid-word but becomes a list-marker trigger only at line-start with a\n following space.\n\n Repro:\n printf '1\\\\. foo\\n' | cargo run --bin pampa -- -t qmd | cargo run --bin pampa -- -t native\n → OrderedList (1, Decimal, Period) [[Plain [Str \"foo\"]]]\n Expected: Para [Str \"1.\", Space, Str \"foo\"]\n\nCommon requirement: writer needs (a) line-start tracking and (b) lookahead\nacross adjacent inline nodes to know whether escaping is needed for the\ncurrent character. This is a larger refactor than escape_markdown can\nabsorb — likely a new pass over the inline sequence in write_para or a\nposition-aware writer-context flag that get_str consults.","status":"open","priority":2,"issue_type":"bug","created_at":"2026-05-01T00:31:59.051282Z","created_by":"cscheid","updated_at":"2026-05-01T00:32:14.786935Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-kk0a","depends_on_id":"bd-21gu","type":"discovered-from","created_at":"2026-05-01T00:31:59.051282Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-kw93","title":"q2 preview epic: ephemeral local hub-client as Q2 replacement for quarto preview","description":"Implement `q2 preview` as a native CLI wrapping an ephemeral local hub-client instance. Uses samod for sync, the q2-preview React format for incremental DOM-stable rendering, and engine: replay for server-records / browser-replays code execution. See claude-notes/plans/2026-05-11-q2-preview-epic.md for architecture, phasing (A: skeleton + standalone SPA serving, B: file-watcher and remap broadening, C: engine execution via replay-on-server, D: polish + parity, E: stretch), and open questions awaiting user sign-off before phase plans are drafted. Branch: feature/q2-preview.","status":"open","priority":1,"issue_type":"epic","created_at":"2026-05-11T14:14:19.736224Z","created_by":"cscheid","updated_at":"2026-05-11T15:41:14.986644Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-kw93","depends_on_id":"bd-hfjj","type":"blocks","created_at":"2026-05-11T15:41:14.986468Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-l173","title":"Match Quarto 1 page-navigation default + styling + icons","description":"see plan","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-04-30T13:14:59.620901Z","created_by":"cscheid","updated_at":"2026-04-30T13:15:09.163538Z","closed_at":"2026-04-30T13:15:09.163374Z","close_reason":"duplicate of bd-bsut (created twice due to initial command parse issue)","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-l6f0","title":"[websites] Honor explicit 'expanded: true' through active resolution","description":"Today resolve_active_state always sets expanded=true on ancestors of an active item, overriding any user-supplied 'expanded: false'. The YAML 'expanded: true' override is parsed but active-state unconditionally sets it to true — which is the right default but means a user cannot force a section collapsed when it contains the active page (rare but legitimate). Fix: honor the YAML value where it is 'true', but treat 'false' as a default that active-state can override up, never down.","status":"open","priority":3,"issue_type":"bug","created_at":"2026-04-24T17:52:38.504797Z","created_by":"cscheid","updated_at":"2026-04-24T17:52:38.504797Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-l6f0","depends_on_id":"bd-9svl","type":"discovered-from","created_at":"2026-04-24T17:52:38.504797Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-lekl","title":"Phase 9 follow-up: deprecate render_qmd in favor of render_page_in_project","description":"Phase 9 sub-phase 9.4 switched the hub-client's renderToHtml from renderQmd to renderPageInProject. render_qmd remains as the single-file fallback inside render_page_in_project plus as a backward-compat surface. After the new entry point has bedded in (one or two release cycles), deprecate render_qmd and remove it.\n\nSteps:\n1. Mark render_qmd with #[deprecated] + console.warn-style notice in the TS wrapper.\n2. Audit all hub-client + tests for direct render_qmd calls; migrate to render_page_in_project.\n3. Wait one release.\n4. Delete the public render_qmd export, fold its remaining body fully into render_single_doc_to_response (private).","status":"open","priority":4,"issue_type":"task","created_at":"2026-04-29T00:32:40.067893Z","created_by":"cscheid","updated_at":"2026-04-29T00:32:40.067893Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-lekl","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-29T00:32:40.067893Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-lekl","depends_on_id":"bd-ayj6","type":"discovered-from","created_at":"2026-04-29T00:32:40.067893Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} diff --git a/claude-notes/plans/2026-05-11-q2-preview-epic.md b/claude-notes/plans/2026-05-11-q2-preview-epic.md new file mode 100644 index 000000000..d65047801 --- /dev/null +++ b/claude-notes/plans/2026-05-11-q2-preview-epic.md @@ -0,0 +1,766 @@ +--- +date: 2026-05-11 +branch: feature/q2-preview +status: v3 — all open items resolved (2026-05-11 review #2). Ready to + spin up the hub-client decomposition sub-epic, after which + Phase A planning can begin. +beads: bd-kw93 (epic). +--- + +# `q2 preview` — Feasibility & Architecture Plan (Epic) + +## Goal + +Build `q2 preview` as a native CLI that wraps an ephemeral local +hub-client instance, using `feature/q2-preview`'s React-driven +incremental renderer as the in-browser view. The user runs +`q2 preview [path]` from a Quarto project directory, a browser tab +opens, and edits to local files appear in the rendered page within a +few hundred milliseconds — *without rebuilding the DOM*, so stateful JS +(Bootstrap menus, MathJax, reveal.js, listings filters) survives every +edit. + +This is the Q2 replacement for Q1's `quarto preview`. The design +explicitly leans on three Q2-specific assets that did not exist in Q1: + +1. **`quarto-hub` + samod + automerge** — already provides + ephemeral-server, file-watching, and websocket reload for free. +2. **The `q2-preview` format (this branch)** — already produces a + post-pipeline AST that the React renderer consumes without + touching the DOM tree on edit. +3. **`engine: replay` + `quarto-trace`** — already captures and + replays engine output deterministically; gives us a clean + "record once on the server, replay everywhere" story for code + execution. + +This plan asserts a *feasible-and-aligned* path, not a final +implementation. Several open questions are listed in §"Open +questions"; they need decisions before phase plans are drafted. + +## Why this is feasible (today's substrate) + +Everything below is in `main` (or in this branch, where called out): + +| Component | Status | Reuse | +|---|---|---| +| `quarto-hub` samod-based sync server | shipped | Drop-in for the ephemeral preview server. Already has `--no-project` / standalone / project modes (`bd-3aga`, `crates/quarto-hub/src/{server,context}.rs`). | +| `FileWatcher` (`crates/quarto-hub/src/watch.rs`) | shipped (`.qmd` only) | Reuse; extend to cover `_quarto.yml`, `_metadata.yml`, `_extensions/`, images, `.tsx`. | +| `StorageManager::new_standalone` + `default_standalone_data_dir` | shipped | Use with an ephemeral temp dir so storage is wipe-on-exit. | +| `quarto-trace-server`'s `include_dir!("$QUARTO_TRACE_VIEWER_EMBED_DIR")` pattern | shipped | Direct precedent for embedding the pared-down hub-client bundle into the `q2` binary. Same `_DIR` override for live UI iteration. | +| `q2-preview` format | this branch | Pipeline + `render_qmd_to_preview_ast` already produce the AST the React renderer consumes (`crates/quarto-core/src/{format,pipeline}.rs`, `ast_transforms.rs:139`). | +| `PreviewRouter` + `Q2PreviewIframe` + `ReactRenderer` | this branch | The React-in-iframe path that survives DOM-stateful JS across edits. Already wired into `hub-client/src/components/render/`. | +| `DocumentProfile` + `ProjectDependencyGraph` (Phase 8) | shipped | Forward + reverse `edges` between docs. Phase 8 gives us *exactly* the "when X changes, re-render Y" mapping we need for cross-doc preview invalidation. | +| `ReplayEngine` + `EngineCapture` in `TraceDocument` | shipped (`bd-45yw`) | The mechanism for "execute once on server, replay forever in WASM". `RenderToFileOptions.replay_capture` and `EngineRegistry::with_replay` are the seams. | +| Initial project → automerge upload | shipped | `HubContext::new` already calls `reconcile_files_with_index` + `sync_all_documents` against a real project. | +| `quarto hub` subcommand | shipped | Confirms the CLI integration pattern we'd mirror for `quarto preview`. | + +What is **missing** is the glue, summarized in §"Phases" below. + +## High-level architecture + +``` +┌──── q2 preview foo.qmd ──────────────────────────────────────────────┐ +│ │ +│ ┌──────────────────────┐ │ +│ │ 1. CLI bootstrap │ Create temp dir; canonicalize project. │ +│ └─────────┬────────────┘ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ 2. samod sync server │ via quarto-hub::server::run_server │ +│ │ (ephemeral data) │ with standalone storage in temp dir. │ +│ └─────────┬────────────┘ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ 3. FileWatcher │ Extended for all preview-relevant files. │ +│ │ + qmd→automerge │ Existing sync.rs handles the diff/merge. │ +│ └─────────┬────────────┘ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ 4. Engine executor │ Server-side native run on .qmd change. │ +│ │ + replay capture │ Writes EngineCapture into automerge. │ +│ │ + invalidation │ Hub-client replays via ReplayEngine. │ +│ └─────────┬────────────┘ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ 5. Static SPA route │ Axum fallback serves the pared-down │ +│ │ (embedded bundle) │ hub-client bundle (include_dir!). │ +│ └─────────┬────────────┘ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ 6. Browser SPA │ Loads → connects samod ws → renders │ +│ │ (pared-down) │ q2-preview React. Updates incrementally. │ +│ └──────────────────────┘ │ +│ │ +│ ┌──────────────────────┐ │ +│ │ 7. Shutdown │ On Ctrl-C: stop watcher, drop samod repo, │ +│ │ (wipe temp dir) │ delete the temp data dir. │ +│ └──────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### Data flow on a single edit + +1. User saves `posts/foo.qmd`. +2. `FileWatcher` fires `Modified(posts/foo.qmd)`. +3. `HubContext::sync_file` reads bytes, forks-at-checkpoint, merges + into the file's automerge doc. This already exists. +4. **(new)** If `foo.qmd` is the currently-previewed page or in its + forward dep-graph closure, the server compares the new file's + code-cell content against the last `EngineCapture`'s + `input_qmd`: + - if code-equal, the existing capture is still valid; nothing + to do (the qmd-text change re-renders via replay). + - if different, the capture is now **stale-but-not-replaced**. + The server records a `staleness: true` marker but **does not + re-execute by default** (per user decision — see Q3 below). + The browser overlays a "Code changed — re-execute?" affordance + and the user opts in explicitly. + New captures only get written when (a) no capture exists yet + for the doc, or (b) the user triggers re-execution via the + affordance / a CLI flag / `_quarto.yml` setting. +5. Hub-client `useAutomergeSync` receives both the qmd-text patch + and the trace patch. +6. Hub-client triggers WASM `render_page_in_project(path)` with the + engine registry overridden to `EngineRegistry::with_replay(cap)`. + The pipeline runs end-to-end *in the browser* without spawning + any subprocesses. The post-pipeline AST flows into + `Q2PreviewIframe`, which morphs only the React subtree that + changed. Bootstrap/MathJax/reveal state persists. + +The crucial detail: **engines run on the server (where they can +shell out); rendering runs in the browser (where the AST→DOM +incrementality lives).** The bridge is the `EngineCapture` carried +through automerge. + +## Phasing + +Phases below are checkpoints, not full sub-plans. Each phase will get +its own dated plan document (and a beads sub-issue under the epic) +once this top-level plan is reviewed. + +### Phase A — Skeleton CLI + standalone serving (no engines) + +Smallest end-to-end vertical slice that proves the architecture. + +- [ ] **A.1** Add `quarto preview` clap subcommand (mirror + `commands/hub.rs`). Args: `[path]` (default: project index), + `--port`, `--no-browser`, `--data-dir `, + `--preview-dir ` (the SPA-from-disk override, same + pattern as `QUARTO_TRACE_VIEWER_DIR`). +- [ ] **A.2** Create `crates/quarto-preview/` (new). Owns the + CLI wiring + the embedded SPA + the preview-specific axum routes + on top of the hub server's router. Depends on `quarto-hub` and + the new `quarto-preview-client` build. +- [ ] **A.3** Add `hub-client` preview-mode build target. A second + Vite entry (`preview.html`) bundles the same code with a + build-time flag `__QUARTO_PREVIEW__` that: + - skips `LoginScreen` / `useAuth`, + - skips `ProjectSelector` / `ProjectSetSetup`, + - skips `FileSidebar` / `Editor` (renders only `Preview`), + - reads `indexDocId` + `wsUrl` from a `` tag emitted by + the server (same trick the hub-client uses for share links). +- [ ] **A.4** Wire `include_dir!("$QUARTO_PREVIEW_EMBED_DIR")` into + `quarto-preview` with the build.rs the same shape as + `quarto-trace-server/build.rs`. Confirm `cargo xtask build_all` + builds the preview bundle into `hub-client/dist-preview/` first. +- [ ] **A.5** `q2 preview` boots: creates a temp `data_dir`, + spawns `quarto-hub::server::run_server` with project mode, + emits the URL, opens the browser, blocks on Ctrl-C. On + shutdown, deletes the temp `data_dir`. +- [ ] **A.6** Manual force-refresh button. A persistent UI + affordance in the preview SPA that re-runs the WASM render + pipeline against the current automerge state. Always visible, + regardless of staleness — this is the user's escape hatch for + cross-doc dependency channels the dep graph doesn't (yet) + encode. Phase C wires the same button to also trigger + server-side engine re-execution when applicable. +- [ ] **A.7** End-to-end smoke test: `q2 preview` on a 2-page + Quarto site with no code cells; editing markdown in either + file updates the preview within 1s; no DOM rebuild + (asserted by a Playwright stable-element-id check); the + manual force-refresh button works. + +**Acceptance:** the loop from §"Data flow" works for documents +*without* engine execution. The force-refresh button works. The +replay/engine work is Phase C. + +### Phase B — File-watcher and remap broadening + +- [ ] **B.1** Extend `FileWatcher`'s `is_qmd_file` filter to a + policy that includes `.qmd`, `_quarto.yml`, `_metadata.yml`, + everything under `_extensions/`, image extensions, and `.tsx` + custom-component files. Keep `.qmd`-only as a feature gate so + the hub binary's existing semantics don't change. +- [ ] **B.2** Decide how `format: html` → `q2-preview` happens in + preview mode (see Open Q1). Add a new `PreviewFormatRemap` knob + on the pipeline config; default off for `render`, on for + `preview`. +- [ ] **B.3** Ensure cross-doc edges from `ProjectDependencyGraph` + invalidate the right pages in the browser. The hub-client + already re-runs `render_page_in_project` when the index doc + changes; we just need to verify that the dep-graph reverse + edges drive that re-run for the *currently-displayed* page + even when a sibling page changes. (May be a no-op if Phase 8's + cache invalidation already covers this; needs investigation.) +- [ ] **B.4** Acceptance: editing `_quarto.yml` re-renders all + open pages; editing `posts/_metadata.yml` re-renders only its + siblings; editing an unrelated sibling re-renders the active + page only when there's a dep edge. + +### Phase C — Engine execution (record-on-demand, replay-otherwise) + +This is the load-bearing phase. The contract is the bridge from +§"Data flow" item 4. + +**Default behavior (per user 2026-05-11):** the server does *not* +automatically re-execute on code-cell change. It detects staleness +and surfaces an affordance. The user opts in. + +- [ ] **C.1** First-time capture trigger. When `q2 preview` opens a + doc with code cells and no existing capture, the server runs + the engine once *eagerly* and writes the resulting + `EngineCapture` into automerge. Until that finishes, the + browser shows a "Executing code…" overlay rendered from the + parse-only AST (code cells appear as their source). +- [ ] **C.2** Staleness detection. On every doc change, the server + parses the .qmd, extracts the code-cell content, and compares + byte-for-byte against the last capture's `input_qmd`. If + different, write a `staleness: true` marker on the doc's index + entry. Do **not** re-execute. +- [ ] **C.3** Capture transport. Add an `engine_capture_id: + Option` field to each text doc's index entry, + pointing at a sibling *binary* doc that stores the gzipped + capture, plus a `staleness: bool`. Both survive reconnect and + sync naturally to the browser via samod. +- [ ] **C.4** Browser-side replay. When `useAutomergeSync` sees a + fresh capture for the active doc, it calls + `render_page_in_project` with the capture surfaced as a new + optional parameter (see Risk 1 below). The render runs the + pipeline with replay; engines in WASM otherwise pass through. +- [ ] **C.5** Stale-capture UX. When `staleness: true`, render + the page *with* the still-valid capture (so the prose/preview + remains live) and overlay a fixed-position "Code has changed — + re-execute?" affordance. The affordance lists which cells + changed (cell IDs from parse). On click, the browser POSTs to + `/api/preview/re-execute`; the server runs the engine, + replaces the capture, clears `staleness`. +- [ ] **C.6** Configuration. `preview.engine: manual | auto | off` + in `_quarto.yml`, with `manual` as the **default**. + - `manual` (default): C.5 behavior. No automatic re-execution. + - `auto`: re-execute on every code-cell change. For users who + want the Q1-style behavior on small docs. + - `off`: never execute. Code cells render as inert source. The + first-time eager run from C.1 is also skipped. +- [ ] **C.7** Per-doc capture cache (in `/captures/`) + keyed by content hash, so swap-and-restore of an open file + doesn't re-execute. Also serves as the "warm cache" when the + user resumes a preview session shortly after closing one. + +**Acceptance:** end-to-end test: +- A `.qmd` with a jupyter cell renders correctly after `q2 + preview` (eager first-time run). +- Editing the prose re-renders without touching the engine. +- Editing the code cell shows the staleness affordance; the + preview keeps rendering with the previous capture. +- Clicking the affordance triggers re-execution and the new + capture renders. +- Setting `preview.engine: auto` reproduces Q1-style behavior. + +### Phase D — Polish & parity + +- [ ] **D.1** Browser-tab-on-startup, `--no-browser`, + port-conflict retry. +- [ ] **D.2** Initial-path resolution: `q2 preview` → project + index; `q2 preview foo.qmd` → that file; with a website + project, resolve the index page via `ProjectIndex`. +- [ ] **D.3** Static-file resources (e.g. CSS in `_extensions/`): + confirm they round-trip through binary-doc sync. +- [ ] **D.4** Diagnostics surface: render errors should overlay + on the preview, not silently fail. (`PreviewErrorOverlay` + already exists; verify its wiring under preview mode.) +- [ ] **D.5** Documentation in `docs/` for user-facing preview + command. + +### Phase E — Stretch (post-MVP) + +- Hot-reload of `.tsx` custom components from disk (not just + through automerge); useful for component developers. +- Multi-window: opening two browser tabs at the same preview URL + should stay in sync via samod (essentially free; verify). +- `q2 preview --share ` to broadcast to a remote sync + server, turning the local preview into a temporary + collaboration session. + +## Open questions + +These are the ones the design intentionally defers. Resolutions +from the 2026-05-11 review are noted inline. Each "**Decision**" +line is settled; the remaining text below explains why. + +### Q1 — Where does `format: html` → `q2-preview` remap happen? + +**Decision: option (c)** — explicit `RenderMode::Preview` threaded +through pipeline config. + +**Options:** +- **(a)** At the orchestrator level: when the preview CLI builds + `HtmlRenderConfig` / `RenderToFileOptions`, substitute the + format-id before the pipeline is built. Surgical; doesn't touch + format.rs. +- **(b)** At `format.rs::Format::from_format_string` time, gated + on a thread-local or context flag set by the preview entry + point. Risk: invisible global state. +- **(c)** Introduce an explicit `RenderMode::Preview` on the + pipeline config, threaded through `build_html_pipeline_stages`, + so each stage knows it's running in preview mode and can pick + the right sub-pipeline. Matches the existing + `ApplyConfig::Single | Project` axis. + +**Recommendation:** (c). It generalizes — the same flag gates +"server should run engines and emit captures", "remap html → +q2-preview", and any future preview-only stages we add. It also +matches how Phase 8 already threads a mode through the pipeline +(Mode A vs Mode B). + +### Q2 — Does the server *also* run the q2-preview pipeline? + +**Decision: option (a)** — server runs engines only; the browser +runs the full q2-preview pipeline via WASM. + +Additional motivation from the user (2026-05-11): a future version +of the q2-preview format will communicate AST changes *back* into +the .qmd source to offer a WYSIWYG-like authoring experience. That +must work in both hub-client and `q2 preview`, which requires the +two surfaces to share the *same* render code path. (b) would fork +that path and break the WYSIWYG round-trip for `q2 preview`. + +This raises the importance of Risk 1 below: the WASM +`render_page_in_project` signature must accept the capture so the +browser-side pipeline can actually run with replay. + +### Q3 — Engine invalidation policy + +**Decision: per-document staleness *detection*, no automatic +re-execution.** The server detects staleness and surfaces an +affordance; the user explicitly opts into re-execution +(`preview.engine: manual` is the default — see Phase C.6). + +Rationale from the user (2026-05-11): code execution can take a +long time. Automatic triggering on every code-cell save is too +disruptive. *Knowing* the capture is stale is valuable — the user +gets a visible cue to re-run when they're ready — but the +automatic path is reserved for users who explicitly opt in +(`preview.engine: auto`). + +Open sub-question (not blocking the epic): what counts as a "code +cell change"? The current proposal is byte-equality of cell +content (matching `ReplayEngine`'s miss policy). Whitespace-only +diffs would trip staleness; that may be acceptable, or it may want +to be smarter later. Defer to Phase C planning. + +### Q4 — Hub-client visibility gating in preview mode + +**Decision: option (c)** — refactor hub-client so the preview app +imports only the components it needs. + +User rationale (2026-05-11): this dovetails with the build-time +concerns in §"Build-time concerns / artifact ordering" below. +Shipping a second Vite-bundled entry from inside hub-client +preserves the current monolithic dependency situation where +"build `q2 preview` Rust" implies "build the full hub-client +SPA." Decomposing hub-client into reusable libraries lets the +preview SPA depend only on what it needs and gives us a cleaner +bootstrapping story. + +This is a larger refactor than originally scoped — it likely +predates the rest of Phase A, or runs in parallel as its own +sub-epic. See §"Build-time concerns" for the implications. + +### Q5 — Project vs single-doc mode + +**Decision: confirmed** — auto-discover project from cwd; allow +`q2 preview --no-project file.qmd` as an escape hatch. Mirrors +`quarto hub` / `quarto render`. + +### Q6 — What about output formats other than html? + +**Decision: HTML-only for MVP.** Q2 is currently HTML-only across +the board (q2-preview, q2-debug, q2-slides all build on HTML). +PDF preview will eventually need a separate mechanism (probably +watching the artifact dir and reloading a PDF viewer iframe), but +that's a future epic and not blocking. + +### Q7 — Security & sandboxing + +**Decision: strict by default.** Bind to 127.0.0.1; refuse +non-loopback hosts unless `--insecure-allow-network` is passed +explicitly; print a stern warning when it is. + +User rationale (2026-05-11): the security posture is more +important in Q2 than in Q1 because the q2-preview UI will +eventually let users *retrigger code execution from the +webpage* (the staleness affordance — Phase C.5) and, further +out, mutate the .qmd source via the WYSIWYG mode mentioned in +Q2. Both turn the preview port into a remote-code-execution +endpoint that anyone-on-the-network could trigger if we bind +beyond loopback. The stricter posture isn't paranoid — it's +matching the actual capability surface. + +## Build-time concerns / artifact ordering + +This concern surfaced in the 2026-05-11 review and is large +enough to deserve its own section. It interacts with Q4 +(hub-client visibility gating) and Risk 1 (WASM signature). + +### The problem + +The preview SPA shares code with hub-client and with the rest of +the Quarto pipeline. The build graph is: + +``` +1. cargo build --target wasm32-unknown-unknown + -p wasm-quarto-hub-client # produces .wasm +2. wasm-bindgen # produces JS+TS glue +3. cd hub-client && npm run build:all # bundles SPA (uses .wasm) +4. cargo build -p quarto-preview \ + (with QUARTO_PREVIEW_EMBED_DIR set) # embeds the SPA +5. cargo build -p quarto # links quarto-preview +``` + +There is **no Cargo-level cycle**. `quarto-preview` (native) +depends on `quarto-hub`; the SPA depends on +`wasm-quarto-hub-client` (wasm32). The two never link against +each other. But the build *order* is: +`wasm32 cargo → wasm-bindgen → npm → native cargo`. + +The user's worry was that "building `q2 preview` requires +building `q2` first" — which is only true if the embedded SPA's +WASM ends up depending transitively on so much of the workspace +that any Rust change forces the whole cascade to re-run. +Bluntly: if a change to `pampa` or `quarto-core` requires +rebuilding the WASM, *then* the SPA, *then* the native binary +that embeds the SPA, iteration cycles are painful. + +### How `quarto-trace-server` handles it (precedent) + +We already solved a smaller version of this for the trace +viewer. `crates/quarto-trace-server/build.rs`: + +- Looks for `trace-viewer/dist/index.html`. +- If present, embeds that directory via `cargo:rustc-env`. +- If absent, generates a *placeholder* `index.html` in `OUT_DIR` + that tells the user to run `cargo xtask build-trace-viewer`, + and embeds the placeholder. +- Always emits `cargo:rerun-if-changed=` for every file under + the real dist (so `cargo build` re-embeds when the SPA + rebuilds, but doesn't *fail* if it's missing). + +`cargo xtask build-trace-viewer` then runs `npm run build` in +`trace-viewer/`. `cargo xtask build-all` chains the full +sequence. Iteration: dev runs the Vite dev server and points the +binary at it via `QUARTO_TRACE_VIEWER_DIR=...`; release builds +do the full cascade. + +This pattern works because `trace-viewer/` is a *standalone* TS +project — no shared Rust code. The SPA bundle never needs to be +rebuilt because of a Rust change. + +### Why preview is harder + +The preview SPA *does* share Rust code (via +`wasm-quarto-hub-client`). A change to `pampa` requires: + +- rebuild wasm-bindgen output → rebuild SPA → re-embed → relink + `quarto-preview` → relink `quarto`. + +Every step is slow on a fresh build. Caching helps in the steady +state but the dev-cycle penalty for cross-cutting Rust changes +is real. + +### Proposed bootstrap strategy + +1. **`quarto-preview/build.rs` mirrors `quarto-trace-server/`.** + Placeholder fallback when the SPA isn't built. `cargo build` + always succeeds; binaries built without the SPA cascade carry + a "preview SPA not built" placeholder and refuse to start the + server with a helpful error. + +2. **Two-tier dev iteration:** + - **UI iteration:** Vite dev server at `localhost:5173` with + `QUARTO_PREVIEW_DIR=...` pointing the running `q2 preview` + at the dev server. No Rust rebuild needed. + - **Rust iteration on preview-specific code:** placeholder + bundle, runtime override fallback. No npm rebuild needed. + - **Cross-cutting Rust changes that affect the WASM + surface:** full `cargo xtask build-preview` rebuild. This + is the slow path, but it's slow *for a reason* — + correctness across the WASM boundary. + +3. **`cargo xtask build-preview` chains the sequence:** + ``` + 1. cargo xtask build-wasm # wasm-quarto-hub-client → pkg/ + 2. (cd hub-client && npm run build:preview) + 3. QUARTO_PREVIEW_EMBED_DIR=… cargo build -p quarto-preview + ``` + And `cargo xtask build-all` extends to include the preview + chain (similar to how it currently extends to include + hub-client + trace-viewer). + +4. **Decompose hub-client (resolves Q4 + this concern).** Split + the parts the preview SPA needs (``, + ``, `Q2PreviewIframe`, the render-time + services like `wasmRenderer`, `automergeSync`, + `assetWalker`) into their own npm workspace package(s) that + *both* hub-client and the preview SPA import. Two + consequences: + - hub-client (the editor) and the preview SPA each have a + `build:preview` / `build:hub` script that produces a + *separate* dist. Touching editor code never rebuilds the + preview SPA, and vice versa. + - Changes to `wasm-quarto-hub-client` invalidate both + bundles, but that's correct — they both consume it. + + This is a non-trivial hub-client refactor and is properly + tracked as its own pre-epic, since the decomposition is also + useful independent of `q2 preview` (clearer code ownership, + smaller hub-client editor bundle, separable test surfaces). + +### Implications for phasing + +- **Phase A** still ships first, but its first task is + "decompose hub-client + define preview SPA package boundary", + which is itself a substantial sub-epic. Phase A as originally + written assumed a single new build target inside hub-client; + this update changes it to a workspace-level reshape. +- The placeholder-bundle pattern from `quarto-trace-server` is + copied wholesale; this is mechanical work. +- Q4 stops being "build flag vs runtime flag vs library refactor" + — it's the library-refactor option as the *enabling* step for + the rest of the plan. + +### What this is *not* + +This isn't a proposal to publish hub-client pieces to npm. The +shared packages live in the workspace (already-existing +`@quarto/quarto-sync-client` is the model). The reshape is +about boundaries inside the monorepo, not about external +distribution. + +## Things explicitly out of scope (for the epic) + +- **PDF preview** — separate epic; see Q6. +- **Shiny / observable runtime preview** — Q1 has special-cased + paths for these. Q2's engine model doesn't yet, and replay + doesn't apply to interactive runtimes. Future work. +- **Multi-user collaborative preview** — phase E mentions + `--share`, but real collaboration belongs in `quarto hub`, not + `quarto preview`. +- **`freeze` integration** — once `freeze` lands (it shares the + profile-checkpoint substrate per the website epic), preview + should honor frozen captures. Out of scope for the MVP because + `freeze` itself isn't shipped. +- **Hot-reload of Lua filters in `_extensions/`** — phase B covers + watching, but actually re-running them on demand may surface + subtle ordering issues with the running pipeline. Treat as a + D-phase polish item. + +## Risks + +1. **WASM pipeline ↔ EngineCapture wiring depth.** Phase C requires + the WASM entry point `render_page_in_project` to accept an + `EngineRegistry` override. Today it doesn't. The override has + to be plumbed through `wasm-quarto-hub-client`'s + `RenderToHtmlRenderer` / `Pass2Renderer` chain. Phase 2C of the + q2-preview plans already plumbed configuration through these, + so the seam is reachable, but the registry type isn't + `Serialize` — we'd have to either lift the capture (which is + serializable) and reconstruct the registry browser-side, or + widen the WASM signature to take the capture directly. The + latter is preferred. + +2. **Cross-doc invalidation completeness.** Phase 8's dependency + graph handles sidebar/prev-next/body-link/nav-dependency edges + today. It does *not* know about, e.g., `include:` shortcodes, + which are an `IncludeEntry` channel on the profile but not + currently wired into the dependency-graph builder. + + User clarification (2026-05-11): the audit here is + *feature-based*, not Q1-parity-based. Q1's preview is itself + limited / best-effort; the goal is to enumerate all the kinds + of cross-document dependencies Q2 *should* track (given + DocumentProfile + edges) and decide which the MVP covers + versus defers. The list at minimum needs to consider: + `include` shortcodes, listing content globs (already partly + in the graph), `bibliography`/`csl` paths, `theme` SCSS imports, + shared resources, and `_extensions/` Lua filters. Each is a + separate edge channel; some belong in the dep-graph builder, + some are file-watch-only. + +3. **DOM-stability under React's reconciler.** The whole pitch of + q2-preview is that stateful JS survives edits. Phase 2C tests + this for callouts/theorems, but the matrix of state-preserving + widgets (Bootstrap dropdowns, MathJax, reveal slides, Leaflet + maps) is larger and not yet exhaustively tested. Likely + surfaces only when real users try it. + +4. **Engine runtime discovery.** Today's `q2 render` discovers + Jupyter kernelspecs via plan `2026-05-04-jupyter-kernelspec- + discovery-and-errors.md`. The preview command needs the same + plumbing — running engines requires resolved kernelspecs. + Shouldn't be net-new work but is a hard dep. + +5. **Initial sync time on large projects.** `HubContext::new` + does an initial scan + push of *every* file in the project to + automerge. For a 500-page project this is non-trivial and the + user is waiting at a blank screen. Phase A ought to surface + progress (or page-by-page lazy-loading) before the first + real-world test. + +## Resolved review items (2026-05-11) + +The original draft asked the user to sign off on a series of +questions. The results are folded into the Open Questions +section above and into the body of the plan, but for the +historical record: + +- **Q1** — pipeline-mode flag: option (c) (explicit `RenderMode`). +- **Q2** — server runs engines only; browser runs full pipeline. + Confirmed; additional WYSIWYG motivation noted. +- **Q3** — per-document staleness *detection*; no automatic + re-execution. Default `preview.engine: manual`. +- **Q4** — decompose hub-client (option c). Larger than originally + scoped; tied to build-time concerns. +- **Q5** — project mode with `--no-project` escape: confirmed. +- **Q6** — HTML-only for MVP: confirmed. +- **Q7** — strict bind-to-loopback default: confirmed; rationale + strengthened by the future code-execution + WYSIWYG surfaces. +- **Phasing A→B→C→D**: confirmed. +- **Crate layout** (separate `quarto-preview` crate): accepted in + principle, but the hub-client decomposition (Q4) may force a + larger reshape than originally envisioned. The "separate crate" + remains the target; the path to get there has more shape. +- **Embed strategy** (`include_dir!` precedent): confirmed; the + full design of how this interacts with build ordering is now + the §"Build-time concerns" section. + +## Resolutions from 2026-05-11 review #2 + +1. **WASM signature**: option (a). Widen + `render_page_in_project` to take an optional `EngineCapture`; + WASM constructs `EngineRegistry::with_replay` internally. + This plan is already complex enough without the more general + override seam. + +2. **First-time eager run**: option (i). Eager engine run on + first open of a never-previewed doc; subsequent code-cell + changes surface the staleness affordance, never auto-execute. + + Rationale (user, 2026-05-11): Quarto already offers controls + for users who want fast first-render — they can mark + individual code cells `execute: false` (or set + `execute: false` at the doc level). Documents that don't opt + out get a real preview on first open. The pain point this + plan avoids is *repeated* re-execution on every edit, not + one-off first-render cost. + +3. **Cross-doc dep audit**: deferred. Phase B covers the + channels already encoded on `DocumentProfile` (the cheap + ones). A full audit becomes a follow-up issue. This implies + a new permanent affordance — see "Force-refresh invariant" + below. + +4. **Force-refresh invariant** (emerged from #3). Because we are + knowingly deferring some cross-doc dependency channels, the + preview UI **must always offer a manual "force re-render" + button**, independent of staleness state. This is the user's + escape hatch when our dep graph misses something. It also + composes naturally with the staleness affordance — both end + up rendering the same "click here to re-do work" surface, + just with different copy. + + This is a new requirement, not a Phase-D nice-to-have. Add + to Phase A's acceptance criteria. + +5. **Crate / SPA layout**: the *physical* naming is bikesheddy, + but there is a load-bearing **invariant** the layout must + enforce: + + > The components that render the preview pane inside + > hub-client and the components in the preview SPA must be + > the *same* React components — same source files, same + > imports, same tests. New preview-pane features landing in + > hub-client land in `q2 preview` for free, and vice versa. + + This is the *reason* for the hub-client decomposition. The + package boundary should be drawn so that violating this + invariant requires a deliberate refactor, not an oversight. + Concretely: any time someone wants to add code to "the + preview pane in hub-client," they should be editing the + shared package, not hub-client itself. + + Phase A's hub-client decomposition step will draw boundaries + that enforce this — it's not just about build performance, + it's about *feature parity by construction* between + hub-client's preview pane and the SPA. + +## Recommended next steps + +Three pieces of work hand off cleanly from this epic: + +1. **Hub-client decomposition sub-epic** (separate beads issue, + blocked-by relationship: the q2 preview epic `bd-kw93` + blocks-on this one). Owns the design sprint for package + boundaries, the actual code reshape, and the invariant from + §"Crate / SPA layout" above. This is the longest pole — it + should land before Phase A picks up the SPA-embed mechanics. + +2. **Phase A plan document** (`claude-notes/plans/2026-05-XX- + q2-preview-phase-a.md`, written after item 1's design sprint + sets the package boundaries). Focused on the CLI skeleton, + build.rs placeholder, embedded SPA serving, and the + force-refresh button. Engine-less, so it's a tight first + slice once the decomposition is settled. + +3. **Cross-doc dep audit follow-up** (separate beads issue, + `related` to the epic). Tracks Phase B's Quarto-feature + enumeration: which dependency channels are encoded on + `DocumentProfile` today, which want to be added, and which + stay manual-refresh-only. + +Items 1 and 3 can be filed now without committing to phase-plan +content. Item 2 waits on item 1's outputs. + +## Out-of-band: post-merge cleanup + +This branch (`feature/q2-preview`) carries the `q2-preview` format +plus its phase 1–8 work. Once the preview command lands, the +`format: q2-preview` literal becomes an implementation detail — end +users shouldn't write it themselves. The format identifier should +probably stay accessible (it's useful for debugging and for the +hub-client editor view), but it should be advertised as +preview-internal in docs. + +## Reference material + +- `claude-notes/plans/2026-05-04-q2-preview-plan-{1..8}.md` — the + q2-preview format itself (this branch). +- `claude-notes/plans/2026-04-23-website-project-epic.md` — + established that `quarto preview` would be a local hub-client + instance ("design decision 5"). This plan is the realization of + that promise. +- `claude-notes/plans/2026-04-27-websites-phase-8.md` — Mode A vs + Mode B + dependency graph, the substrate for cross-doc + invalidation. +- `claude-notes/plans/2026-05-03-replay-engine.md` — replay engine + + capture format. +- `claude-notes/plans/2026-02-02-quarto-hub-subcommand.md` — + pattern for adding a new `quarto ` subcommand. +- `claude-notes/plans/2026-03-03-hub-no-local-watch.md` — + standalone-mode and `--data-dir` were added for exactly this + kind of ephemeral-server use case. +- `crates/quarto-trace-server/{build.rs,src/lib.rs}` — the + `include_dir!` SPA-embedding precedent. +- `crates/quarto-hub/src/{server,context,watch,sync}.rs` — the + pieces we wrap. +- `external-sources/quarto-cli/src/command/preview/preview.ts` — + Q1 preview for behavioral parity reference (not a copy target). From 1e95b39029420796c9b4c57dbcc1409443f0645f Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 11 May 2026 12:33:48 -0500 Subject: [PATCH 002/108] docs(q2-preview): hub-client decomposition sub-epic plan (bd-hfjj) Concretizes the bd-hfjj sub-epic of bd-kw93 with package boundaries (two packages: @quarto/preview-renderer + @quarto/preview-runtime), SPA location (top-level q2-preview-spa/), MVP scope (React preview path only; Preview.tsx / slides / debug stay in hub-client), WASM access (existing symlink + per-consumer alias), and a 7-phase sequencing that keeps tests green across each move. This branch holds both this sub-epic plan and the parent epic plan (via the 03fc1238 ancestor commit). Parked off feature/q2-preview while the other workstream there settles. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-11-hub-client-decomposition.md | 780 ++++++++++++++++++ 1 file changed, 780 insertions(+) create mode 100644 claude-notes/plans/2026-05-11-hub-client-decomposition.md diff --git a/claude-notes/plans/2026-05-11-hub-client-decomposition.md b/claude-notes/plans/2026-05-11-hub-client-decomposition.md new file mode 100644 index 000000000..b84897f31 --- /dev/null +++ b/claude-notes/plans/2026-05-11-hub-client-decomposition.md @@ -0,0 +1,780 @@ +--- +date: 2026-05-11 +branch: feature/q2-preview +beads: bd-hfjj (sub-epic of bd-kw93) +status: draft — awaiting user review before implementation begins +--- + +# Hub-client decomposition: shared preview-pane packages for hub-client + q2-preview-spa + +## Goal + +Carve the q2-preview React rendering stack out of `hub-client/` into +two new npm-workspace packages — `@quarto/preview-renderer` (pure +React: framework, q2-preview format components, iframe wrappers) and +`@quarto/preview-runtime` (WASM + automerge glue: `wasmRenderer`, +`automergeSync`, `assetWalker`, user-grammar services). + +Create a new top-level `q2-preview-spa/` skeleton that imports from +those packages and produces a buildable (but non-functional) bundle. + +After this sub-epic, hub-client's preview pane and the future +`q2 preview` SPA will render the q2-preview format through the **same** +React components — the `§Crate / SPA layout` invariant from the epic +([2026-05-11-q2-preview-epic.md](2026-05-11-q2-preview-epic.md)) +is satisfied by construction. + +This is the dependency that blocks Phase A of bd-kw93. No engine +work, no samod wiring, no `crates/quarto-preview/` here — that is +Phase A. + +## Decisions resolved with user (2026-05-11) + +1. **Two shared packages.** `@quarto/preview-renderer` (pure React, + no WASM imports of its own) + `@quarto/preview-runtime` (WASM + glue + automerge sync). Renderer can be unit-tested without + WASM init; runtime owns the side-effecting initialisation. +2. **Top-level `q2-preview-spa/`.** Sibling to `hub-client/` and + `trace-viewer/`. Mirrors trace-viewer's shape. Editor code + cannot leak in by accident. +3. **MVP scope = React preview path only.** Move the framework, + q2-preview components, `Q2PreviewIframe`, `PreviewErrorOverlay`, + `MorphIframe`/`DoubleBufferedIframe`, and the supporting types/ + utils/contexts. **Keep in hub-client:** `Preview.tsx` (HTML + iframe + Monaco-aware scroll/selection sync), `PreviewRouter.tsx` + (format dispatcher), `ReactPreview.tsx`, `ReactRenderer.tsx` + (variant dispatcher), `Q2DebugIframe.tsx`, `ReactAstSlideRenderer + .tsx`, `RevealjsReactAstSlideRenderer.tsx`. These can move later + as the SPA grows; they're not on the bd-hfjj critical path. +4. **WASM access via existing alias.** Keep + `hub-client/wasm-quarto-hub-client → + crates/wasm-quarto-hub-client/pkg` symlink as-is. Each consumer + (hub-client, `q2-preview-spa`, the runtime package's vitest + config) declares the same `wasm-quarto-hub-client` alias in its + Vite/Vitest config. The runtime package's *source* never names + the alias target — only the import string — so it stays + bundler-agnostic. +5. **SPA = skeleton placeholder.** `q2-preview-spa/src/main.tsx` + imports something from `@quarto/preview-renderer` (e.g. + `` rendered with placeholder text) and + produces a buildable `dist/`. No samod, no WASM init, no + automerge — those are Phase A. The skeleton's only job is to + *prove the cross-package boundary works* by linking a second + consumer against the new packages. + +## Workspace layout (end state) + +``` +/q2/ +├── hub-client/ (existing — editor SPA) +│ ├── src/components/ +│ │ ├── Editor.tsx (untouched) +│ │ ├── FileSidebar.tsx (untouched) +│ │ ├── ... (auth, tabs, dialogs — untouched) +│ │ └── render/ +│ │ ├── Preview.tsx (stays; HTML iframe path) +│ │ ├── PreviewRouter.tsx (stays; imports from shared) +│ │ ├── ReactPreview.tsx (stays) +│ │ ├── ReactRenderer.tsx (stays; variant dispatcher) +│ │ ├── q2-debug/ (stays) +│ │ ├── ReactAstSlideRenderer.tsx (stays) +│ │ └── RevealjsReactAstSlideRenderer.tsx (stays) +│ ├── src/services/ +│ │ ├── authService.ts (stays) +│ │ ├── projectStorage.ts (stays) +│ │ ├── presenceService.ts (stays) +│ │ └── ... (other editor-only services) +│ ├── wasm-quarto-hub-client/ (symlink, unchanged) +│ └── vite.config.ts (alias kept; updated entry list) +│ +├── q2-preview-spa/ (NEW — skeleton SPA) +│ ├── package.json (name: q2-preview-spa, private) +│ ├── vite.config.ts (alias to hub-client's WASM symlink) +│ ├── index.html +│ ├── tsconfig.json +│ └── src/ +│ └── main.tsx (~20 lines, placeholder) +│ +├── ts-packages/ +│ ├── preview-renderer/ (NEW — pure React) +│ │ ├── package.json ("@quarto/preview-renderer") +│ │ ├── tsconfig.json +│ │ ├── vitest.config.ts +│ │ ├── vitest.integration.config.ts +│ │ └── src/ +│ │ ├── index.ts (public re-exports) +│ │ ├── framework/ (Ast.tsx, dispatch, registry, types) +│ │ ├── q2-preview/ (entry, dispatchers, registry, +│ │ │ PreviewDocument, blocks/, inlines/, +│ │ │ custom/, contexts, utils) +│ │ ├── iframe/ (Q2PreviewIframe, MorphIframe, +│ │ │ DoubleBufferedIframe) +│ │ ├── overlays/ (PreviewErrorOverlay, +│ │ │ PreviewStaticInfoViews) +│ │ ├── types/ (project, diagnostic, artifactPaths, +│ │ │ sourceInfo, intelligence) +│ │ ├── contexts/ (ThemeContext) +│ │ └── utils/ (vfsPaths, iframeLinkHandlers, +│ │ iframePostProcessor, componentPath, +│ │ stripAnsi) +│ │ +│ ├── preview-runtime/ (NEW — WASM + automerge glue) +│ │ ├── package.json ("@quarto/preview-runtime") +│ │ ├── tsconfig.json +│ │ ├── vitest.config.ts (with WASM alias) +│ │ └── src/ +│ │ ├── index.ts (public re-exports) +│ │ ├── wasmRenderer.ts (initWasm, renderToHtml, +│ │ │ parseQmdToAst, renderPageInProject, +│ │ │ vfsReadFile, vfsAddFile, ...) +│ │ ├── automergeSync.ts (createSyncClient, getFileContent, ...) +│ │ ├── assetWalker.ts (buildAssetManifest) +│ │ └── userGrammar/ (Discovery, Cache, Highlight) +│ │ +│ └── (existing packages unchanged: annotated-qmd, pandoc-types, +│ quarto-automerge-schema, quarto-hub-mcp, quarto-sync-client, +│ sync-test-harness) +│ +└── crates/ + ├── wasm-quarto-hub-client/ (unchanged — WASM source) + └── (no new crates in this sub-epic) +``` + +## File-move catalogue + +The list below is the **complete** authoritative map. If a file +isn't listed, it doesn't move. Paths are relative to repo root. + +### Moving to `ts-packages/preview-renderer/src/` + +**framework/** +- `hub-client/src/components/render/framework/` → `framework/` + (entire subtree — `Ast.tsx`, `dispatch.tsx`, `RegistryContext.tsx`, + `customNode.ts`, `meta.ts`, `plainText.ts`, `types.ts`, + `index.ts`, plus colocated `.test.ts` files) + +**q2-preview/** +- `hub-client/src/components/render/q2-preview/` → `q2-preview/` + (entire subtree — `entry.tsx`, `dispatchers.tsx`, `registry.ts`, + `PreviewDocument.tsx`, `AssetManifestContext.tsx`, + `PreviewContext.tsx`, `NoteNumberingContext.tsx`, + `quartoClasses.ts`, `theoremEnvs.ts`, `utils.tsx`, + `blocks/**`, `inlines/**`, `custom/**`, plus colocated tests + including `registry.test.ts`, `assetWalker.test.ts`, + `*.integration.test.tsx`) + + Note: `assetWalker.ts` itself moves to **preview-runtime** + (it depends on `vfsReadBinaryFile`), but its test + (`assetWalker.test.ts`) moves with it. + +**iframe/** +- `hub-client/src/components/render/Q2PreviewIframe.tsx` → + `iframe/Q2PreviewIframe.tsx` (+ `.integration.test.tsx`) +- `hub-client/src/components/render/MorphIframe.tsx` → + `iframe/MorphIframe.tsx` +- `hub-client/src/components/render/DoubleBufferedIframe.tsx` → + `iframe/DoubleBufferedIframe.tsx` + +**overlays/** +- `hub-client/src/components/render/PreviewErrorOverlay.tsx` → + `overlays/PreviewErrorOverlay.tsx` (+ `.integration.test.tsx`) +- `hub-client/src/components/render/PreviewStaticInfoViews.tsx` → + `overlays/PreviewStaticInfoViews.tsx` + +**types/** — the types used by the moving components +- `hub-client/src/types/project.ts` → `types/project.ts` +- `hub-client/src/types/diagnostic.ts` → `types/diagnostic.ts` +- `hub-client/src/types/artifactPaths.ts` → `types/artifactPaths.ts` +- `hub-client/src/types/sourceInfo.ts` → `types/sourceInfo.ts` +- `hub-client/src/types/intelligence.ts` → `types/intelligence.ts` + +Hub-client still needs these — it will re-import from +`@quarto/preview-renderer`. Audit during Phase 2 to confirm no +editor-only fields leak into these types; if so, split. + +**contexts/** +- `hub-client/src/components/ThemeContext.tsx` → `contexts/ThemeContext.tsx` + +`ViewModeContext.tsx` stays in hub-client (it controls editor +layout — meaningless to the SPA). + +**utils/** — the utils used by the moving components +- `hub-client/src/utils/vfsPaths.ts` → `utils/vfsPaths.ts` +- `hub-client/src/utils/iframeLinkHandlers.ts` → + `utils/iframeLinkHandlers.ts` +- `hub-client/src/utils/iframePostProcessor.ts` → + `utils/iframePostProcessor.ts` +- `hub-client/src/utils/componentPath.ts` → `utils/componentPath.ts` +- `hub-client/src/utils/stripAnsi.ts` → `utils/stripAnsi.ts` + +### Moving to `ts-packages/preview-runtime/src/` + +- `hub-client/src/services/wasmRenderer.ts` → `wasmRenderer.ts` + (+ any colocated tests) +- `hub-client/src/services/automergeSync.ts` → `automergeSync.ts` + (+ tests) +- `hub-client/src/components/render/q2-preview/assetWalker.ts` → + `assetWalker.ts` (the *implementation*; the test moves alongside + it here as well, since it tests the runtime function. Update the + preview-renderer cross-ref note above.) +- `hub-client/src/services/userGrammarDiscovery.ts` → + `userGrammar/Discovery.ts` +- `hub-client/src/services/userGrammarCache.ts` → + `userGrammar/Cache.ts` +- `hub-client/src/services/userGrammarHighlight.ts` → + `userGrammar/Highlight.ts` + +### Staying in hub-client (explicit list, for review) + +To make the boundary review-able, here are the preview-adjacent +files that explicitly **stay** in `hub-client/src/`: + +- `components/render/Preview.tsx` +- `components/render/PreviewRouter.tsx` +- `components/render/ReactPreview.tsx` +- `components/render/ReactRenderer.tsx` +- `components/render/q2-debug/` (entire subtree) +- `components/render/ReactAstSlideRenderer.tsx` +- `components/render/RevealjsReactAstSlideRenderer.tsx` +- `components/render/parity.integration.test.tsx` (compares HTML + vs React path; tests cross both packages — best kept in + hub-client which is where both paths are reachable) +- `hooks/useScrollSync.ts`, `hooks/useSelectionSync.ts` (Monaco- + coupled; not preview-pane-renderable concerns) +- `services/authService.ts`, `projectStorage.ts`, + `presenceService.ts`, etc. (editor only) +- `components/ViewModeContext.tsx` +- All `components/tabs/`, `components/auth/`, `Editor.tsx`, + `FileSidebar.tsx`, etc. + +If anything below feels like it should move, raise it before +Phase 2 — the move sequence depends on this list being final. + +## Workspace plumbing (mechanical, applies to both new packages) + +Each new package mirrors the existing `ts-packages/quarto-sync-client/` +pattern. + +**`package.json` shape:** + +```jsonc +{ + "name": "@quarto/preview-renderer", // or preview-runtime + "version": "0.0.1", + "private": true, + "type": "module", + "main": "dist/index.js", + "types": "src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "source": "./src/index.ts", // Vite picks this via the + "import": "./dist/index.js" // 'source' resolve.condition + } + }, + "files": ["dist"], + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist", + "test": "vitest run", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:watch": "vitest" + }, + "dependencies": { /* see per-package below */ }, + "devDependencies": { "typescript": "~5.9.3", "vitest": "^4.0.17" } +} +``` + +**preview-renderer dependencies:** +- `react`, `react-dom`, `morphdom` (peer-style: declared dep so + TypeScript can find types; hub-client and the SPA carry their + own copies via npm hoisting) +- `@quarto/pandoc-types` (for AST type imports if used) +- `katex` (already in root devDeps; if any q2-preview block uses + it directly add here) + +**preview-runtime dependencies:** +- `@quarto/quarto-sync-client`, `@quarto/quarto-automerge-schema` + (workspace `*`) +- `@automerge/automerge`, `@automerge/automerge-repo` (the runtime + is what holds the automerge integration) +- `web-tree-sitter` (used by `userGrammar/Highlight`) +- React is **not** a runtime dep — the runtime is pure JS/TS, no + components + +**`tsconfig.json` shape** (cribs from `quarto-sync-client/tsconfig.json`): + +```jsonc +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], // renderer needs DOM; runtime needs DOM (assetWalker uses URL) + "jsx": "react-jsx", // renderer only; omit for runtime + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "verbatimModuleSyntax": true, + "outDir": "./dist", + "declaration": true, + "rootDir": "./src" + }, + "include": ["src"], + "exclude": [ + "src/**/*.test.ts", + "src/**/*.test.tsx", + "src/**/*.integration.test.ts", + "src/**/*.integration.test.tsx" + ] +} +``` + +**`vitest.config.ts` for preview-runtime** must declare the WASM +alias so unit tests for `wasmRenderer` and `automergeSync` resolve +the same way Vite does in app bundles: + +```ts +import { defineConfig } from 'vitest/config' +import path from 'path' + +export default defineConfig({ + resolve: { + conditions: ['source', 'import', 'module', 'browser', 'default'], + alias: { + 'wasm-quarto-hub-client': path.resolve( + __dirname, + '../../hub-client/wasm-quarto-hub-client/wasm_quarto_hub_client.js' + ), + }, + }, + test: { + environment: 'jsdom', + // mirror hub-client/vitest.config.ts as needed + }, +}) +``` + +**Top-level workspace `package.json`** already has +`workspaces: ["ts-packages/*", "hub-client", "trace-viewer", +"q2-demos/*"]`. Adding `q2-preview-spa` to the list is a one-line +change. + +## `q2-preview-spa/` skeleton (decision 5) + +Minimal, but real: + +``` +q2-preview-spa/ +├── package.json +├── vite.config.ts +├── tsconfig.json +├── index.html +└── src/main.tsx +``` + +`package.json`: + +```jsonc +{ + "name": "q2-preview-spa", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@quarto/preview-renderer": "*", + "@quarto/preview-runtime": "*", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.1.1", + "vite": "^7.2.4", + "vite-plugin-wasm": "^3.5.0", + "typescript": "~5.9.3" + } +} +``` + +`vite.config.ts` mirrors `hub-client/vite.config.ts`'s alias and +`source` condition, with a single entry `index.html`. No proxy +configuration (Phase A adds it). + +`src/main.tsx` (the placeholder — proves the import chain works): + +```tsx +import { createRoot } from 'react-dom/client' +import { PreviewErrorOverlay } from '@quarto/preview-renderer' + +createRoot(document.getElementById('root')!).render( + +) +``` + +(`PreviewErrorOverlay`'s actual prop shape is to be checked when +the move lands; the placeholder content is interchangeable.) + +## Phasing + +Seven phases, each independently mergeable and verifiable. Per +project policy (CLAUDE.md), after each phase: `cargo xtask verify +--skip-rust-tests` from repo root, plus per-package +`npm run typecheck && npm run test` for any affected package. + +The TDD policy applies as "tests-stay-green-across-move." We are +not adding behavior; we are relocating it. Each phase's invariant +is: the same set of tests passes before and after the move. + +### Phase 0 — Pre-flight (no code changes) + +- [ ] Verify the starting workspace builds clean. + `cd hub-client && npm run build:all && npm run test:ci`. +- [ ] Confirm the file lists in "File-move catalogue" against + current `git ls-files`. Patch the plan with any drift. +- [ ] Decide on the test-helper / mock files under + `src/test-utils/` and `src/__mocks__/`: which move with + `preview-renderer`, which with `preview-runtime`, which + stay in hub-client. Update catalogue. + +**Acceptance:** workspace builds and tests pass on the current +branch; catalogue is final. + +### Phase 1 — Empty workspace packages + +- [ ] Create `ts-packages/preview-renderer/` with `package.json`, + `tsconfig.json`, `vitest.config.ts`, + `vitest.integration.config.ts`, empty `src/index.ts` + exporting nothing. +- [ ] Create `ts-packages/preview-runtime/` with the same + skeleton, including the WASM alias in `vitest.config.ts`. +- [ ] Run `npm install` from repo root. Confirm new workspaces + register (`npm ls @quarto/preview-renderer @quarto/preview-runtime`). +- [ ] Add a placeholder `test.ts` in each `src/` that runs a + trivial assertion; confirm `npm test --workspace + @quarto/preview-renderer` (and runtime) pass. +- [ ] `cd hub-client && npm install` to re-link; confirm hub-client + can `import {} from '@quarto/preview-renderer'` (compiles to + a no-op). Update `tsconfig.app.json` references if hub-client + uses project references for workspace deps (it currently + doesn't, but verify). + +**Acceptance:** both new packages exist, are recognized by npm +workspaces, have passing test commands, and are importable from +hub-client (even though they export nothing yet). + +### Phase 2 — Move types, contexts, and utils + +The lowest-risk moves. Pure data + pure functions; no React tree. + +- [ ] Move the five `types/` files to + `ts-packages/preview-renderer/src/types/`. +- [ ] Move `ThemeContext.tsx` to + `ts-packages/preview-renderer/src/contexts/`. +- [ ] Move the five `utils/` files to + `ts-packages/preview-renderer/src/utils/`. +- [ ] Add re-exports to `ts-packages/preview-renderer/src/index.ts`. +- [ ] Update every importer in hub-client (likely ~30-50 files). + `find hub-client/src -name '*.tsx' -o -name '*.ts' | xargs + grep -l "from '\.\./.*types/project'"` etc., rewrite to + `from '@quarto/preview-renderer'`. +- [ ] Audit: do any of these types/utils carry editor-specific + fields that the preview pane never uses? If yes, split. If + not, the import-path rewrite is the whole change. + +**Acceptance:** +- `cd hub-client && npm run typecheck && npm run test:ci && + npm run build:all` passes. +- `cargo xtask verify --skip-rust-tests` passes. +- `npm test --workspace @quarto/preview-renderer` passes. + +### Phase 3 — Move framework/ + +- [ ] Move `components/render/framework/` entire subtree to + `ts-packages/preview-renderer/src/framework/`. +- [ ] Re-export through `src/index.ts`. +- [ ] Update import paths in everything that imports from + `components/render/framework/...`. Confirm that the framework + tests (`framework/*.test.ts`) run via + `npm test --workspace @quarto/preview-renderer`. + +**Acceptance:** same as Phase 2. + +### Phase 4 — Move q2-preview/, iframe wrappers, overlays + +The biggest single move (~50 files including tests). Do it in one +phase so the q2-preview registry, dispatchers, and components stay +internally consistent. + +- [ ] Move `components/render/q2-preview/` (entire subtree). +- [ ] Move `Q2PreviewIframe.tsx`, `MorphIframe.tsx`, + `DoubleBufferedIframe.tsx` to + `preview-renderer/src/iframe/`. +- [ ] Move `PreviewErrorOverlay.tsx`, + `PreviewStaticInfoViews.tsx` to + `preview-renderer/src/overlays/`. +- [ ] Move colocated tests including the integration tests: + `Q2PreviewIframe.integration.test.tsx`, + `q2-preview.integration.test.tsx`, + `PreviewDocument.integration.test.tsx`, + `custom-components.integration.test.tsx`, + `entry.integration.test.tsx`, + `PreviewErrorOverlay.integration.test.tsx`. +- [ ] Configure `vitest.integration.config.ts` in + preview-renderer to run these. May need jsdom setup + borrowed from hub-client's integration config. +- [ ] Update hub-client imports: `Preview.tsx`, + `PreviewRouter.tsx`, `ReactPreview.tsx`, + `ReactRenderer.tsx` rewrite to import from + `@quarto/preview-renderer`. +- [ ] **Decide where `parity.integration.test.tsx` lives.** + Recommendation: stays in hub-client (it compares the HTML + iframe path — owned by hub-client — against the React + path — owned by the shared package — so it's a hub-client + consumer-level test). Document the call. + +**Acceptance:** +- Same as Phase 2. +- All q2-preview integration tests run from the new package and + pass. +- hub-client's preview pane still renders correctly in + `npm run dev`. Manual browser check: open a Quarto project, + see q2-preview format render. (Per CLAUDE.md §End-to-end + verification — record what you saw.) + +### Phase 5 — Move services to preview-runtime + +- [ ] Move `wasmRenderer.ts` → `preview-runtime/src/wasmRenderer.ts`. +- [ ] Move `automergeSync.ts` → `preview-runtime/src/automergeSync.ts`. +- [ ] Move `assetWalker.ts` (from `q2-preview/`) → + `preview-runtime/src/assetWalker.ts`. +- [ ] Move the three `userGrammar*` files → + `preview-runtime/src/userGrammar/` (renamed: `Discovery.ts`, + `Cache.ts`, `Highlight.ts`). +- [ ] Move colocated tests. +- [ ] `preview-renderer`'s `assetWalker.ts` consumers (only the + Q2PreviewIframe boot path) now import from + `@quarto/preview-runtime`. This is a renderer→runtime + dependency — declare it in `preview-renderer/package.json`'s + `dependencies` (workspace `*`). + + Tradeoff to note: this means preview-renderer is no longer + "pure React with no WASM transitive." It pulls in + preview-runtime, which pulls in the WASM module. That's + acceptable because (a) the WASM module is lazy-loaded at + runtime via `initWasm()`, (b) Vite can tree-shake unused + runtime exports for SPA consumers that don't call them. + The "renderer is purely React" framing in Decision 1 was + aspirational; the practical split is "renderer = React + components that drive a render, runtime = the things they + call out to." That's still useful as a split. + +- [ ] Update every hub-client import of these services to + `@quarto/preview-runtime`. +- [ ] Configure `preview-runtime/vitest.config.ts` (and + `vitest.wasm.config.ts` if needed) with the WASM alias. + Confirm WASM-using tests run. + +**Acceptance:** +- Same as Phase 4, plus: +- `npm test --workspace @quarto/preview-runtime` passes, + including any WASM-using tests. +- hub-client still builds and tests cleanly. +- Manual: `npm run dev` in hub-client → preview pane renders → + WASM init happens → q2-preview format displays. + +### Phase 6 — Create `q2-preview-spa/` skeleton + +- [ ] Create the directory + files per "skeleton" section above. +- [ ] Add `"q2-preview-spa"` to root `package.json` `workspaces`. +- [ ] `npm install` from root; confirm vite picks up the new + workspace. +- [ ] `cd q2-preview-spa && npm run build`. Confirm `dist/` + contains `index.html` and a bundled JS file. Open it + in a browser; the placeholder text renders. +- [ ] Inspect the built bundle: `du -sh dist/` and `ls + dist/assets/`. Confirm that none of hub-client's editor + code (e.g. no Monaco, no `Editor`, no `FileSidebar`) is in + the bundle. This validates the §invariant the easy way: + because the SPA imports only from shared packages, editor + code *cannot* be transitively pulled in. + +**Acceptance:** +- `q2-preview-spa/dist/index.html` builds and renders in a + browser. +- Bundle does not contain editor-only code (Monaco, Editor, + auth, sidebar). Record `du`/`grep` evidence in the commit + message. + +### Phase 7 — `cargo xtask verify` integration, cleanup, docs + +- [ ] Extend `cargo xtask verify` so it also runs `npm run + typecheck && npm test` for the two new packages. Today it + does `cd hub-client && npm run test:ci`; the simplest + extension is to also run `npm test --workspaces + --if-present` from repo root, which picks up the new + packages automatically. +- [ ] Add a `cd q2-preview-spa && npm run build` step to + `cargo xtask verify` (or to a follow-on `build-preview` + task — bd-kw93 Phase A will introduce the cargo-xtask + command formally; for now just ensure the SPA build runs + in CI). +- [ ] Delete files left behind in hub-client. Audit + `hub-client/src/` for any orphaned imports or empty dirs. +- [ ] Update `hub-client/changelog.md` per the project's + hub-client commit convention. +- [ ] Optional: update `CLAUDE.md`'s "Workspace structure" + section to reflect the two new packages and the SPA. + +**Acceptance:** +- `cargo xtask verify` succeeds from a fresh clone (modulo the + `npm install` step from `.claude/rules/worktrees.md`). +- Hub-client builds, tests, and runs the preview pane. +- `q2-preview-spa` builds and the placeholder renders. +- All paths reviewed. + +## Invariant enforcement + +The epic's §Crate / SPA layout invariant says: + +> The components that render the preview pane inside hub-client +> and the components in the preview SPA must be the *same* React +> components — same source files, same imports, same tests. + +After this sub-epic the invariant is enforced **at the +`Q2PreviewIframe + framework + q2-preview registry` layer**, which +is the layer that turns an AST into rendered DOM. Both surfaces +go through the same code to: + +- dispatch on AST node types (`framework/dispatch.tsx`), +- render each block / inline / custom node + (`q2-preview/blocks/`, `inlines/`, `custom/`), +- mount and morph the iframe (`iframe/Q2PreviewIframe.tsx`, + `MorphIframe.tsx`), +- surface render errors (`overlays/PreviewErrorOverlay.tsx`). + +The *shells* that wrap that core (hub-client's `PreviewRouter` + +`ReactRenderer` + scroll-sync hooks; the SPA's `main.tsx`) are +allowed to differ — they carry surface-specific concerns +(editor coupling vs none). The invariant is about content +rendering, not chrome. + +If someone later adds a new block to the q2-preview format, they +have only one place to add it (`preview-renderer/src/q2-preview/ +blocks/`), and both surfaces pick it up for free. If they want to +add a new variant *router* (e.g., a new format like q2-poster), +that's a hub-client concern unless and until the SPA needs it +too — at which point the variant moves up into the shared +package. + +This is "feature parity by construction" in the sense the epic +described. + +## Risks + +1. **`parity.integration.test.tsx` ownership.** This test compares + the HTML iframe path (`Preview.tsx` — staying in hub-client) + against the React path (moving to shared). It can only live in + a place that has both paths reachable. Plan: keep it in + hub-client. Note this in the test's header comment. + +2. **WASM test infrastructure in `preview-runtime`.** The + `wasmRenderer` tests are currently colocated in hub-client and + use hub-client's vitest configs (`vitest.config.ts` + + `vitest.wasm.config.ts`). Moving them requires re-creating + that WASM-init plumbing in the new package. Potential gotchas: + - WASM module path resolution under jsdom (Vitest needs the + alias and the `wasm()` plugin). + - The shared `__mocks__/` and `test-utils/` directories + contain helpers (`fake-indexeddb` shim, jsdom-wasm setup) + that may need to be moved or duplicated. + + Mitigation: do Phase 5 last so the renderer is stable first; + if WASM tests blow up, hub-client still has its preview pane + working (because the runtime is still consumed via the new + package and Vite's app-level alias works). + +3. **Cross-package circular imports.** The proposed dep is + `preview-renderer → preview-runtime` (for `vfsReadBinaryFile` + used by `assetWalker`). One-way. If a future addition + introduces `preview-runtime → preview-renderer` (e.g., the + runtime wanting a React error type from the renderer), that's + a circular dep. **Rule:** keep runtime React-free. If a type + needs to be shared in both directions, it belongs in a third + tiny package or in the renderer's `types/`. + +4. **Tests-stay-green is a strong invariant.** The + tree-shake-based bundling can occasionally hide a missing + re-export until production build. Each phase's acceptance + includes `npm run build:all`, not just typecheck — the prod + build uses project references in `tsc -b` and is stricter, + matching the CLAUDE.md note about hub-client builds. + +5. **`hub-client/changelog.md` update cadence.** The CLAUDE.md + policy is "two-commit workflow" — change, then changelog + referencing the change's hash. Each phase here that touches + hub-client triggers that. Easy to forget when phases are + rapid-fire. Mitigation: include "update changelog.md" as the + final checklist item in each hub-client-touching phase. + +6. **Surprise editor-only fields in shared types.** `project.ts`, + `diagnostic.ts`, etc. were authored for hub-client's full UI. + Some fields may be editor-flavored (e.g. tab state, presence + metadata). Audit during Phase 2; split or rename if needed. + +7. **Discovery during the move.** It is likely that one or two + files don't fit cleanly on either side and want a small refactor + (split a function, lift a helper). When this happens: file a + `discovered-from:bd-hfjj` beads issue, do the smallest split + necessary to unblock the move, and continue. Do not absorb + open-ended refactor into this sub-epic. + +## Out of scope + +- Anything in `crates/quarto-preview/` (Phase A of bd-kw93). +- The `build.rs` placeholder pattern (Phase A). +- The `__QUARTO_PREVIEW__` build-time flag (the epic v3 proposed + this for a single-vite-entry approach; we don't need it because + the SPA is its own workspace package). +- Moving `Preview.tsx` (HTML iframe path) to shared. It depends + on Monaco-aware scroll sync. If the future SPA needs an HTML + iframe path, that's a follow-up sub-epic. +- Moving slides (`ReactAstSlideRenderer.tsx`, + `RevealjsReactAstSlideRenderer.tsx`) to shared. Same reasoning. +- Moving q2-debug to shared. Same. +- Decomposing `hub-client/components/render/` further (e.g. into + per-format packages). The current split is "shared = q2-preview + format only"; that's the load-bearing one. +- npm publish for the new packages. They are workspace-internal + (`"private": true`). + +## Open follow-ups (to file as related beads issues after plan approval) + +1. **(`bd-hfjj-fu1` etc.)** Move slides + q2-debug to shared + packages, so the SPA can render those formats too. Probably a + single follow-up sub-epic. +2. **Refactor `useScrollSync` / `useSelectionSync`** to be + Monaco-optional, so an SPA-flavored scroll sync becomes + possible. Needed if `q2 preview` ever shows a side-by-side + editor. +3. **Audit shared types** for editor-only fields and split as + needed. May surface during Phase 2's audit step. + +## Reference + +- Epic: `claude-notes/plans/2026-05-11-q2-preview-epic.md` +- Beads: `bd-hfjj` (this sub-epic), `bd-kw93` (parent), `bd-56b0` + (related: cross-doc dep audit) +- Workspace pattern model: `ts-packages/quarto-sync-client/` +- WASM consumption model: `hub-client/vite.config.ts:38-40` (the + alias) and `hub-client/wasm-quarto-hub-client` (the symlink) +- Build-script precedent (used by Phase A, not this sub-epic): + `crates/quarto-trace-server/build.rs` From 12347a54c81f99a30cb2cd9ee4506398f9ab5134 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 10:00:40 -0500 Subject: [PATCH 003/108] feat(q2-preview): scaffold preview-renderer + preview-runtime workspaces (bd-hfjj Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creates two empty workspace packages mirroring the ts-packages/quarto-sync-client pattern: - `@quarto/preview-renderer` — pure React; jsdom-based integration config; deps on react, react-dom, morphdom. - `@quarto/preview-runtime` — WASM + automerge glue; both vitest configs declare the `wasm-quarto-hub-client` alias pointing at hub-client's existing symlink; deps on @automerge/automerge, @automerge/automerge-repo, @quarto/quarto-sync-client, @quarto/quarto-automerge-schema, web-tree-sitter. Each package ships an `export {}` index, a trivial placeholder test (green on both), and a small jsdom polyfill `test-utils/setup.ts` so its integration config is self-contained. Both pass `test`, `typecheck`, and `build`. Phase 0 pre-flight is also captured in this commit: - catalogue audit added customRegistry, atomicCustomNodes, and utils/sourceInfo to the renderer move list (drift discovered by grep against the current tree); - test-helper placement section records dispositions (mockSyncClient/mockWasm → runtime; visibility/setup/userSettings stay in hub-client); - plan status flipped from "draft — awaiting review" to "approved; Phases 0–1 complete; Phase 2 next". Hub-client probe imports (added to main.tsx, compiled cleanly through both `tsc --noEmit` and `vite build`, then reverted) verified that both packages resolve via npm's workspace symlinks + tsc's bundler moduleResolution + vite's `source` condition. No changes to hub-client landed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-11-hub-client-decomposition.md | 100 ++++++++++++++---- package-lock.json | 44 ++++++++ ts-packages/preview-renderer/package.json | 45 ++++++++ ts-packages/preview-renderer/src/index.ts | 1 + .../preview-renderer/src/placeholder.test.ts | 7 ++ .../preview-renderer/src/test-utils/setup.ts | 30 ++++++ ts-packages/preview-renderer/tsconfig.json | 31 ++++++ ts-packages/preview-renderer/vitest.config.ts | 16 +++ .../vitest.integration.config.ts | 17 +++ ts-packages/preview-runtime/package.json | 43 ++++++++ ts-packages/preview-runtime/src/index.ts | 1 + .../preview-runtime/src/placeholder.test.ts | 7 ++ .../preview-runtime/src/test-utils/setup.ts | 18 ++++ ts-packages/preview-runtime/tsconfig.json | 28 +++++ ts-packages/preview-runtime/vitest.config.ts | 23 ++++ .../vitest.integration.config.ts | 21 ++++ 16 files changed, 409 insertions(+), 23 deletions(-) create mode 100644 ts-packages/preview-renderer/package.json create mode 100644 ts-packages/preview-renderer/src/index.ts create mode 100644 ts-packages/preview-renderer/src/placeholder.test.ts create mode 100644 ts-packages/preview-renderer/src/test-utils/setup.ts create mode 100644 ts-packages/preview-renderer/tsconfig.json create mode 100644 ts-packages/preview-renderer/vitest.config.ts create mode 100644 ts-packages/preview-renderer/vitest.integration.config.ts create mode 100644 ts-packages/preview-runtime/package.json create mode 100644 ts-packages/preview-runtime/src/index.ts create mode 100644 ts-packages/preview-runtime/src/placeholder.test.ts create mode 100644 ts-packages/preview-runtime/src/test-utils/setup.ts create mode 100644 ts-packages/preview-runtime/tsconfig.json create mode 100644 ts-packages/preview-runtime/vitest.config.ts create mode 100644 ts-packages/preview-runtime/vitest.integration.config.ts diff --git a/claude-notes/plans/2026-05-11-hub-client-decomposition.md b/claude-notes/plans/2026-05-11-hub-client-decomposition.md index b84897f31..5e9ad1e0d 100644 --- a/claude-notes/plans/2026-05-11-hub-client-decomposition.md +++ b/claude-notes/plans/2026-05-11-hub-client-decomposition.md @@ -1,8 +1,9 @@ --- date: 2026-05-11 -branch: feature/q2-preview +updated: 2026-05-13 +branch: beads/bd-hfjj-hub-client-decomposition-shared beads: bd-hfjj (sub-epic of bd-kw93) -status: draft — awaiting user review before implementation begins +status: approved 2026-05-13; Phases 0–1 complete; Phase 2 next --- # Hub-client decomposition: shared preview-pane packages for hub-client + q2-preview-spa @@ -199,13 +200,31 @@ editor-only fields leak into these types; if so, split. layout — meaningless to the SPA). **utils/** — the utils used by the moving components -- `hub-client/src/utils/vfsPaths.ts` → `utils/vfsPaths.ts` -- `hub-client/src/utils/iframeLinkHandlers.ts` → - `utils/iframeLinkHandlers.ts` -- `hub-client/src/utils/iframePostProcessor.ts` → +- `hub-client/src/utils/vfsPaths.ts` (+ `.test.ts`) → + `utils/vfsPaths.ts` +- `hub-client/src/utils/iframeLinkHandlers.ts` + (+ `.integration.test.ts`) → `utils/iframeLinkHandlers.ts` +- `hub-client/src/utils/iframePostProcessor.ts` + (+ `.test.ts`, `.integration.test.ts`) → `utils/iframePostProcessor.ts` -- `hub-client/src/utils/componentPath.ts` → `utils/componentPath.ts` -- `hub-client/src/utils/stripAnsi.ts` → `utils/stripAnsi.ts` +- `hub-client/src/utils/componentPath.ts` (+ `.test.ts`) → + `utils/componentPath.ts` +- `hub-client/src/utils/stripAnsi.ts` (+ `.test.ts`) → + `utils/stripAnsi.ts` +- `hub-client/src/utils/customRegistry.ts` (+ `.test.ts`) → + `utils/customRegistry.ts` + *(Phase 0.2 drift: imported by `q2-preview/entry.tsx` (moving) and + `q2-debug/entry.tsx` (staying). After the move, q2-debug imports + from `@quarto/preview-renderer`.)* +- `hub-client/src/utils/atomicCustomNodes.ts` → + `utils/atomicCustomNodes.ts` + *(Phase 0.2 drift: imported by `framework/dispatch.tsx`.)* +- `hub-client/src/utils/sourceInfo.ts` (+ `.test.ts`) → + `utils/sourceInfo.ts` + *(Phase 0.2 drift: distinct from `types/sourceInfo.ts`; imported + by `framework/dispatch.tsx`.)* + +Note: `types/project.test.ts` moves with `types/project.ts`. ### Moving to `ts-packages/preview-runtime/src/` @@ -436,40 +455,75 @@ is: the same set of tests passes before and after the move. ### Phase 0 — Pre-flight (no code changes) -- [ ] Verify the starting workspace builds clean. +- [x] Verify the starting workspace builds clean. `cd hub-client && npm run build:all && npm run test:ci`. -- [ ] Confirm the file lists in "File-move catalogue" against + *(2026-05-13: build green; 84/84 tests pass across + `test`, `test:integration`, `test:wasm`.)* +- [x] Confirm the file lists in "File-move catalogue" against current `git ls-files`. Patch the plan with any drift. -- [ ] Decide on the test-helper / mock files under + *(2026-05-13: added `customRegistry`, `atomicCustomNodes`, + `utils/sourceInfo` to renderer moves; documented colocated + tests inline; flagged q2-debug's reverse import of + `customRegistry` after the move.)* +- [x] Decide on the test-helper / mock files under `src/test-utils/` and `src/__mocks__/`: which move with `preview-renderer`, which with `preview-runtime`, which stay in hub-client. Update catalogue. + *(2026-05-13: dispositions recorded in §Test-helper + placement below.)* **Acceptance:** workspace builds and tests pass on the current -branch; catalogue is final. +branch; catalogue is final. ✓ + +### Test-helper placement (Phase 0.3 resolution) + +Cross-referencing imports across `hub-client/src` shows only three +files consume `test-utils/`: + +| Helper | Consumers | Disposition | +|---|---|---| +| `test-utils/mockSyncClient.ts` | `services/automergeSync.test.ts` *(moving to preview-runtime)* | **Moves with preview-runtime** → `ts-packages/preview-runtime/src/test-utils/mockSyncClient.ts` | +| `test-utils/mockWasm.ts` | none in source tree; exported via `test-utils/index.ts` for renderer-style tests | **Moves with preview-runtime** → `ts-packages/preview-runtime/src/test-utils/mockWasm.ts` (mocks the WASM renderer the runtime owns) | +| `test-utils/visibility.ts` | `hooks/useAutomergeSync.test.ts`, `services/presenceService.test.ts` *(both stay in hub-client)* | **Stays in hub-client** | +| `test-utils/setup.ts` | `vitest.integration.config.ts` (hub-client) | **Stays in hub-client**; new packages get their own small `setup.ts` per `vitest.integration.config.ts` (pure jsdom polyfills — duplication is cheap and keeps boundaries clean). Promote to a shared `@quarto/test-setup` package only if the file grows. | +| `test-utils/index.ts` | barrel for the above | Hub-client's copy keeps `visibility.ts` exports only; preview-runtime grows its own barrel. | +| `__mocks__/userSettings.ts` | hub-client editor settings (auto-mock) | **Stays in hub-client** (editor-only). | + +`components/render/experimental-components/` (`.tsx.txt` and `.jsx` +scratch files under `new/`) is untouched by the move and stays in +hub-client — those files are not imported by the build. ### Phase 1 — Empty workspace packages -- [ ] Create `ts-packages/preview-renderer/` with `package.json`, +- [x] Create `ts-packages/preview-renderer/` with `package.json`, `tsconfig.json`, `vitest.config.ts`, `vitest.integration.config.ts`, empty `src/index.ts` exporting nothing. -- [ ] Create `ts-packages/preview-runtime/` with the same - skeleton, including the WASM alias in `vitest.config.ts`. -- [ ] Run `npm install` from repo root. Confirm new workspaces +- [x] Create `ts-packages/preview-runtime/` with the same + skeleton, including the WASM alias in `vitest.config.ts` + and `vitest.integration.config.ts`. +- [x] Run `npm install` from repo root. Confirm new workspaces register (`npm ls @quarto/preview-renderer @quarto/preview-runtime`). -- [ ] Add a placeholder `test.ts` in each `src/` that runs a + *(2026-05-13: both registered as workspace symlinks.)* +- [x] Add a placeholder `test.ts` in each `src/` that runs a trivial assertion; confirm `npm test --workspace @quarto/preview-renderer` (and runtime) pass. -- [ ] `cd hub-client && npm install` to re-link; confirm hub-client - can `import {} from '@quarto/preview-renderer'` (compiles to - a no-op). Update `tsconfig.app.json` references if hub-client - uses project references for workspace deps (it currently - doesn't, but verify). + *(2026-05-13: 1 test/package, both green; typecheck + build + also succeed.)* +- [x] Confirm hub-client can `import '@quarto/preview-renderer'` + and `import '@quarto/preview-runtime'`. Update + `tsconfig.app.json` references if hub-client uses project + references for workspace deps. + *(2026-05-13: temporary probe imports in `main.tsx` compiled + cleanly through `tsc --noEmit` and `vite build`. Reverted + after verification. tsconfig.app.json needed no changes — + hub-client doesn't use TS project references for workspace + deps; it relies on npm's `node_modules/@quarto/...` symlinks + + tsc's "bundler" `moduleResolution`.)* **Acceptance:** both new packages exist, are recognized by npm workspaces, have passing test commands, and are importable from -hub-client (even though they export nothing yet). +hub-client (even though they export nothing yet). ✓ ### Phase 2 — Move types, contexts, and utils diff --git a/package-lock.json b/package-lock.json index 62b695c17..842a9c034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2889,6 +2889,14 @@ "resolved": "ts-packages/pandoc-types", "link": true }, + "node_modules/@quarto/preview-renderer": { + "resolved": "ts-packages/preview-renderer", + "link": true + }, + "node_modules/@quarto/preview-runtime": { + "resolved": "ts-packages/preview-runtime", + "link": true + }, "node_modules/@quarto/quarto-automerge-schema": { "resolved": "ts-packages/quarto-automerge-schema", "link": true @@ -7699,6 +7707,42 @@ "typescript": "^5.4.2" } }, + "ts-packages/preview-renderer": { + "name": "@quarto/preview-renderer", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "morphdom": "^2.7.8", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "jsdom": "^26.0.0", + "typescript": "~5.9.3", + "vitest": "^4.0.17" + } + }, + "ts-packages/preview-runtime": { + "name": "@quarto/preview-runtime", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@automerge/automerge": "^2.2.9", + "@automerge/automerge-repo": "^2.5.1", + "@quarto/quarto-automerge-schema": "*", + "@quarto/quarto-sync-client": "*", + "web-tree-sitter": "^0.26.8" + }, + "devDependencies": { + "jsdom": "^26.0.0", + "typescript": "~5.9.3", + "vitest": "^4.0.17" + } + }, "ts-packages/quarto-automerge-schema": { "name": "@quarto/quarto-automerge-schema", "version": "0.0.1", diff --git a/ts-packages/preview-renderer/package.json b/ts-packages/preview-renderer/package.json new file mode 100644 index 000000000..33b959309 --- /dev/null +++ b/ts-packages/preview-renderer/package.json @@ -0,0 +1,45 @@ +{ + "name": "@quarto/preview-renderer", + "version": "0.0.1", + "private": true, + "description": "Pure-React components that render Quarto's q2-preview format. Shared by hub-client and the q2-preview SPA.", + "license": "MIT", + "author": { + "name": "Posit PBC" + }, + "type": "module", + "main": "dist/index.js", + "types": "src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "source": "./src/index.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist", + "test": "vitest run", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:watch": "vitest" + }, + "dependencies": { + "morphdom": "^2.7.8", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "jsdom": "^26.0.0", + "typescript": "~5.9.3", + "vitest": "^4.0.17" + } +} diff --git a/ts-packages/preview-renderer/src/index.ts b/ts-packages/preview-renderer/src/index.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/ts-packages/preview-renderer/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/ts-packages/preview-renderer/src/placeholder.test.ts b/ts-packages/preview-renderer/src/placeholder.test.ts new file mode 100644 index 000000000..6f807cb15 --- /dev/null +++ b/ts-packages/preview-renderer/src/placeholder.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('@quarto/preview-renderer placeholder', () => { + it('package is wired up', () => { + expect(1 + 1).toBe(2); + }); +}); diff --git a/ts-packages/preview-renderer/src/test-utils/setup.ts b/ts-packages/preview-renderer/src/test-utils/setup.ts new file mode 100644 index 000000000..89c32c62f --- /dev/null +++ b/ts-packages/preview-renderer/src/test-utils/setup.ts @@ -0,0 +1,30 @@ +import '@testing-library/jest-dom'; +import { vi } from 'vitest'; + +if (!globalThis.crypto?.randomUUID) { + const cryptoPolyfill = { + ...globalThis.crypto, + randomUUID: () => 'test-uuid-' + Math.random().toString(36).substring(2, 11), + } as Crypto; + Object.defineProperty(globalThis, 'crypto', { value: cryptoPolyfill }); +} + +if (!globalThis.ResizeObserver) { + globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })); +} + +if (!globalThis.IntersectionObserver) { + globalThis.IntersectionObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + root: null, + rootMargin: '', + thresholds: [], + takeRecords: () => [], + })) as unknown as typeof IntersectionObserver; +} diff --git a/ts-packages/preview-renderer/tsconfig.json b/ts-packages/preview-renderer/tsconfig.json new file mode 100644 index 000000000..6bf0cb41d --- /dev/null +++ b/ts-packages/preview-renderer/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "jsx": "react-jsx", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist", + "src/**/*.test.ts", + "src/**/*.test.tsx", + "src/**/*.integration.test.ts", + "src/**/*.integration.test.tsx", + "src/test-utils/**" + ] +} diff --git a/ts-packages/preview-renderer/vitest.config.ts b/ts-packages/preview-renderer/vitest.config.ts new file mode 100644 index 000000000..c149fe531 --- /dev/null +++ b/ts-packages/preview-renderer/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + conditions: ['source', 'import', 'module', 'browser', 'default'], + }, + test: { + environment: 'node', + include: ['src/**/*.test.ts', 'src/**/*.test.tsx'], + exclude: [ + 'src/**/*.integration.test.ts', + 'src/**/*.integration.test.tsx', + ], + passWithNoTests: true, + }, +}); diff --git a/ts-packages/preview-renderer/vitest.integration.config.ts b/ts-packages/preview-renderer/vitest.integration.config.ts new file mode 100644 index 000000000..08007a822 --- /dev/null +++ b/ts-packages/preview-renderer/vitest.integration.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + conditions: ['source', 'import', 'module', 'browser', 'default'], + }, + test: { + environment: 'jsdom', + globals: true, + include: [ + 'src/**/*.integration.test.ts', + 'src/**/*.integration.test.tsx', + ], + setupFiles: ['./src/test-utils/setup.ts'], + passWithNoTests: true, + }, +}); diff --git a/ts-packages/preview-runtime/package.json b/ts-packages/preview-runtime/package.json new file mode 100644 index 000000000..c30982210 --- /dev/null +++ b/ts-packages/preview-runtime/package.json @@ -0,0 +1,43 @@ +{ + "name": "@quarto/preview-runtime", + "version": "0.0.1", + "private": true, + "description": "WASM + automerge glue for Quarto's q2-preview renderer. Shared by hub-client and the q2-preview SPA.", + "license": "MIT", + "author": { + "name": "Posit PBC" + }, + "type": "module", + "main": "dist/index.js", + "types": "src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "source": "./src/index.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist", + "test": "vitest run", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:watch": "vitest" + }, + "dependencies": { + "@automerge/automerge": "^2.2.9", + "@automerge/automerge-repo": "^2.5.1", + "@quarto/quarto-automerge-schema": "*", + "@quarto/quarto-sync-client": "*", + "web-tree-sitter": "^0.26.8" + }, + "devDependencies": { + "jsdom": "^26.0.0", + "typescript": "~5.9.3", + "vitest": "^4.0.17" + } +} diff --git a/ts-packages/preview-runtime/src/index.ts b/ts-packages/preview-runtime/src/index.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/ts-packages/preview-runtime/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/ts-packages/preview-runtime/src/placeholder.test.ts b/ts-packages/preview-runtime/src/placeholder.test.ts new file mode 100644 index 000000000..50093ca45 --- /dev/null +++ b/ts-packages/preview-runtime/src/placeholder.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('@quarto/preview-runtime placeholder', () => { + it('package is wired up', () => { + expect(1 + 1).toBe(2); + }); +}); diff --git a/ts-packages/preview-runtime/src/test-utils/setup.ts b/ts-packages/preview-runtime/src/test-utils/setup.ts new file mode 100644 index 000000000..b05d756df --- /dev/null +++ b/ts-packages/preview-runtime/src/test-utils/setup.ts @@ -0,0 +1,18 @@ +import 'fake-indexeddb/auto'; +import { vi } from 'vitest'; + +if (!globalThis.crypto?.randomUUID) { + const cryptoPolyfill = { + ...globalThis.crypto, + randomUUID: () => 'test-uuid-' + Math.random().toString(36).substring(2, 11), + } as Crypto; + Object.defineProperty(globalThis, 'crypto', { value: cryptoPolyfill }); +} + +if (!globalThis.ResizeObserver) { + globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })); +} diff --git a/ts-packages/preview-runtime/tsconfig.json b/ts-packages/preview-runtime/tsconfig.json new file mode 100644 index 000000000..eb5224288 --- /dev/null +++ b/ts-packages/preview-runtime/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist", + "src/**/*.test.ts", + "src/**/*.integration.test.ts", + "src/test-utils/**" + ] +} diff --git a/ts-packages/preview-runtime/vitest.config.ts b/ts-packages/preview-runtime/vitest.config.ts new file mode 100644 index 000000000..0fac223eb --- /dev/null +++ b/ts-packages/preview-runtime/vitest.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + resolve: { + conditions: ['source', 'import', 'module', 'browser', 'default'], + alias: { + // Mirror hub-client/vite.config.ts. The wasm-quarto-hub-client symlink + // lives in hub-client/ for now; preview-runtime points at it through + // the workspace. When the SPA is added in Phase 6 it does the same. + 'wasm-quarto-hub-client': path.resolve( + __dirname, + '../../hub-client/wasm-quarto-hub-client/wasm_quarto_hub_client.js', + ), + }, + }, + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + exclude: ['src/**/*.integration.test.ts'], + passWithNoTests: true, + }, +}); diff --git a/ts-packages/preview-runtime/vitest.integration.config.ts b/ts-packages/preview-runtime/vitest.integration.config.ts new file mode 100644 index 000000000..c1ed5f837 --- /dev/null +++ b/ts-packages/preview-runtime/vitest.integration.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + resolve: { + conditions: ['source', 'import', 'module', 'browser', 'default'], + alias: { + 'wasm-quarto-hub-client': path.resolve( + __dirname, + '../../hub-client/wasm-quarto-hub-client/wasm_quarto_hub_client.js', + ), + }, + }, + test: { + environment: 'jsdom', + globals: true, + include: ['src/**/*.integration.test.ts'], + setupFiles: ['./src/test-utils/setup.ts'], + passWithNoTests: true, + }, +}); From 5d8bd2b3331de8bd51b25be24efba500bc810650 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 10:15:41 -0500 Subject: [PATCH 004/108] refactor(q2-preview): move types + utils to preview-renderer (bd-hfjj Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves 5 type modules and 7 utility modules out of hub-client into the new @quarto/preview-renderer workspace, behind sub-path exports: ts-packages/preview-renderer/src/types/ project.ts (+ test), diagnostic.ts, artifactPaths.ts, sourceInfo.ts, intelligence.ts ts-packages/preview-renderer/src/utils/ vfsPaths.ts (+ test), iframeLinkHandlers.ts (+ integration test), componentPath.ts (+ test), stripAnsi.ts (+ test), customRegistry.ts (+ test), atomicCustomNodes.ts, sourceInfo.ts (+ test) `git mv` preserves history. All 64 importers across 44 hub-client files are rewritten from `..//types/` / `..//utils/` to `@quarto/preview-renderer/types/` / `/utils/`. The package's exports map uses wildcard sub-paths because `types/diagnostic` and `types/intelligence` both legitimately export `Diagnostic` / `DiagnosticDetail` (the former @deprecated), and a single barrel would force every importer to alias one side. Two files originally scheduled for Phase 2 were deferred after audit: - `components/ThemeContext.tsx` — no moving file consumes it, but it imports `services/preferences/` (localStorage). The sound move is a DI refactor, deferred until the SPA actually needs theme switching. Tracked as follow-up `bd-hfjj-fu-theme`. - `utils/iframePostProcessor.ts` — imports `vfsReadFile` / `vfsReadBinaryFile` from `services/wasmRenderer`, which itself moves in Phase 5. Moving the postprocessor with wasmRenderer then is cleaner than adding a DI seam now. Its remaining internal import of `./vfsPaths` is updated to `@quarto/preview-renderer/utils/vfsPaths`. Hub-client's three vitest configs gain a `@quarto/preview-renderer` → `ts-packages/preview-renderer/src` alias. Vitest doesn't honor the package.json `source` condition the same way Vite's prod build does on fresh clones; the alias matches the existing sync-client / automerge-schema pattern. preview-renderer adds `@quarto/quarto-automerge-schema` as a workspace dep (required by `types/project`), and its own vitest.config.ts gains the same alias so its tests resolve workspace deps to source. Hub-client's test:ci, build:all, and `cargo xtask verify --skip-rust-tests` all pass. Preview-renderer's unit (81 tests across 7 files) and integration (7 tests) suites pass. Snapshot tests: no snapshot files changed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-11-hub-client-decomposition.md | 94 +++++++++++++++---- hub-client/src/App.tsx | 2 +- hub-client/src/components/DevHarness.tsx | 2 +- hub-client/src/components/Editor.tsx | 6 +- .../FileSidebar.integration.test.tsx | 2 +- hub-client/src/components/FileSidebar.tsx | 4 +- hub-client/src/components/OutlinePanel.tsx | 2 +- hub-client/src/components/ProjectSelector.tsx | 2 +- hub-client/src/components/ProjectSetSetup.tsx | 2 +- hub-client/src/components/render/Preview.tsx | 4 +- .../PreviewErrorOverlay.integration.test.tsx | 2 +- .../components/render/PreviewErrorOverlay.tsx | 4 +- .../src/components/render/PreviewRouter.tsx | 6 +- .../render/PreviewStaticInfoViews.tsx | 4 +- .../render/ReactAstSlideRenderer.tsx | 2 +- .../src/components/render/ReactPreview.tsx | 6 +- .../src/components/render/ReactRenderer.tsx | 4 +- .../src/components/render/framework/Ast.tsx | 2 +- .../render/framework/RegistryContext.tsx | 2 +- .../components/render/framework/dispatch.tsx | 4 +- .../src/components/render/q2-debug/entry.tsx | 2 +- .../render/q2-preview/Q2PreviewIframe.tsx | 2 +- .../render/q2-preview/assetWalker.ts | 2 +- .../components/render/q2-preview/entry.tsx | 4 +- hub-client/src/components/tabs/ProjectTab.tsx | 2 +- .../src/debug/hooks/useLocalProjects.ts | 2 +- .../src/debug/services/localProjects.ts | 2 +- hub-client/src/hooks/useAutomergeSync.test.ts | 2 +- hub-client/src/hooks/useAutomergeSync.ts | 2 +- hub-client/src/hooks/useCursorToSlide.ts | 2 +- hub-client/src/hooks/useIntelligence.ts | 2 +- hub-client/src/hooks/useProjectSet.ts | 2 +- hub-client/src/hooks/useSectionThumbnails.ts | 2 +- hub-client/src/hooks/useSlideThumbnails.tsx | 2 +- hub-client/src/services/debugApi.test.ts | 2 +- hub-client/src/services/debugApi.ts | 4 +- .../src/services/intelligenceService.ts | 6 +- hub-client/src/services/monacoProviders.ts | 2 +- hub-client/src/services/projectStorage.ts | 2 +- hub-client/src/services/resourceService.ts | 2 +- hub-client/src/services/wasmRenderer.ts | 4 +- hub-client/src/test-utils/mockWasm.ts | 2 +- .../src/utils/diagnosticToMonaco.test.ts | 4 +- hub-client/src/utils/diagnosticToMonaco.ts | 6 +- hub-client/src/utils/fileTree.ts | 2 +- hub-client/src/utils/iframePostProcessor.ts | 2 +- hub-client/vitest.config.ts | 4 + hub-client/vitest.integration.config.ts | 1 + hub-client/vitest.wasm.config.ts | 1 + ts-packages/preview-renderer/package.json | 11 +++ ts-packages/preview-renderer/src/index.ts | 11 +++ .../src/types/artifactPaths.ts | 0 .../preview-renderer}/src/types/diagnostic.ts | 0 .../src/types/intelligence.ts | 0 .../src/types/project.test.ts | 0 .../preview-renderer}/src/types/project.ts | 0 .../preview-renderer}/src/types/sourceInfo.ts | 0 .../src/utils/atomicCustomNodes.ts | 0 .../src/utils/componentPath.test.ts | 0 .../src/utils/componentPath.ts | 0 .../src/utils/customRegistry.test.ts | 0 .../src/utils/customRegistry.ts | 0 .../iframeLinkHandlers.integration.test.ts | 0 .../src/utils/iframeLinkHandlers.ts | 0 .../src/utils/sourceInfo.test.ts | 0 .../preview-renderer}/src/utils/sourceInfo.ts | 0 .../src/utils/stripAnsi.test.ts | 0 .../preview-renderer}/src/utils/stripAnsi.ts | 0 .../src/utils/vfsPaths.test.ts | 0 .../preview-renderer}/src/utils/vfsPaths.ts | 0 ts-packages/preview-renderer/vitest.config.ts | 9 ++ .../vitest.integration.config.ts | 7 ++ 72 files changed, 184 insertions(+), 84 deletions(-) rename {hub-client => ts-packages/preview-renderer}/src/types/artifactPaths.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/types/diagnostic.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/types/intelligence.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/types/project.test.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/types/project.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/types/sourceInfo.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/atomicCustomNodes.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/componentPath.test.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/componentPath.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/customRegistry.test.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/customRegistry.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/iframeLinkHandlers.integration.test.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/iframeLinkHandlers.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/sourceInfo.test.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/sourceInfo.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/stripAnsi.test.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/stripAnsi.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/vfsPaths.test.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/vfsPaths.ts (100%) diff --git a/claude-notes/plans/2026-05-11-hub-client-decomposition.md b/claude-notes/plans/2026-05-11-hub-client-decomposition.md index 5e9ad1e0d..6ff8a90f3 100644 --- a/claude-notes/plans/2026-05-11-hub-client-decomposition.md +++ b/claude-notes/plans/2026-05-11-hub-client-decomposition.md @@ -3,7 +3,7 @@ date: 2026-05-11 updated: 2026-05-13 branch: beads/bd-hfjj-hub-client-decomposition-shared beads: bd-hfjj (sub-epic of bd-kw93) -status: approved 2026-05-13; Phases 0–1 complete; Phase 2 next +status: approved 2026-05-13; Phases 0–2 complete; Phase 3 next --- # Hub-client decomposition: shared preview-pane packages for hub-client + q2-preview-spa @@ -193,8 +193,27 @@ Hub-client still needs these — it will re-import from `@quarto/preview-renderer`. Audit during Phase 2 to confirm no editor-only fields leak into these types; if so, split. -**contexts/** -- `hub-client/src/components/ThemeContext.tsx` → `contexts/ThemeContext.tsx` +**contexts/** — **deferred (2026-05-13)** + +`ThemeContext.tsx` was scheduled to move, but inspection during +Phase 2 surfaced two facts: + +1. **No moving file uses it.** Only `App.tsx`, `ProjectSelector + .tsx`, and `Editor.tsx` consume `ThemeProvider` / `useTheme` — + all three stay in hub-client. +2. **It is coupled to `services/preferences/`** (localStorage- + backed user prefs), which is editor-only. + +Moving it now would either pollute preview-renderer with +localStorage I/O or break the import. Neither is desirable; we +also don't *need* it moved for Phase 4. The sound refactor — +DI'ing `getPreference`/`setPreference` through props — is a +small but real design change that has no caller yet. **Decision: +keep `ThemeContext.tsx` in hub-client; do the DI refactor when +the SPA actually needs a theme provider.** Note this as a +follow-up issue (see §Open follow-ups). The `./contexts/*` +sub-path export was removed from `preview-renderer/package.json` +accordingly. `ViewModeContext.tsx` stays in hub-client (it controls editor layout — meaningless to the SPA). @@ -206,7 +225,13 @@ layout — meaningless to the SPA). (+ `.integration.test.ts`) → `utils/iframeLinkHandlers.ts` - `hub-client/src/utils/iframePostProcessor.ts` (+ `.test.ts`, `.integration.test.ts`) → - `utils/iframePostProcessor.ts` + **deferred to Phase 5** (imports `vfsReadFile` / + `vfsReadBinaryFile` from `services/wasmRenderer`, which itself + moves to `@quarto/preview-runtime` in Phase 5; moving it + together avoids either a wrong-direction + preview-renderer→hub-client back-import or a premature + DI refactor against iframe wrappers that themselves move in + Phase 4) - `hub-client/src/utils/componentPath.ts` (+ `.test.ts`) → `utils/componentPath.ts` - `hub-client/src/utils/stripAnsi.ts` (+ `.test.ts`) → @@ -529,26 +554,43 @@ hub-client (even though they export nothing yet). ✓ The lowest-risk moves. Pure data + pure functions; no React tree. -- [ ] Move the five `types/` files to +- [x] Move the five `types/` files to `ts-packages/preview-renderer/src/types/`. -- [ ] Move `ThemeContext.tsx` to - `ts-packages/preview-renderer/src/contexts/`. -- [ ] Move the five `utils/` files to + *(2026-05-13: project + project.test, diagnostic, + artifactPaths, sourceInfo, intelligence moved with `git mv`.)* +- [x] **Deferred** `ThemeContext.tsx` — see §contexts/ above. + Tracked as `bd-hfjj-fu-theme`. +- [x] Move the `utils/` files to `ts-packages/preview-renderer/src/utils/`. -- [ ] Add re-exports to `ts-packages/preview-renderer/src/index.ts`. -- [ ] Update every importer in hub-client (likely ~30-50 files). - `find hub-client/src -name '*.tsx' -o -name '*.ts' | xargs - grep -l "from '\.\./.*types/project'"` etc., rewrite to - `from '@quarto/preview-renderer'`. -- [ ] Audit: do any of these types/utils carry editor-specific - fields that the preview pane never uses? If yes, split. If - not, the import-path rewrite is the whole change. + *(2026-05-13: 7 of 8 moved — vfsPaths, iframeLinkHandlers, + componentPath, stripAnsi, customRegistry, atomicCustomNodes, + sourceInfo. iframePostProcessor deferred to Phase 5 because + it imports from services/wasmRenderer.)* +- [x] Add re-exports to `ts-packages/preview-renderer/src/index.ts`. + *(2026-05-13: minimal — only a design comment for now. + Sub-path exports in package.json handle the Phase-2 surface; + barrel exports grow with Phases 3–4.)* +- [x] Update every importer in hub-client. + *(2026-05-13: 64 imports across 44 files rewritten to + `@quarto/preview-renderer/types/` and `/utils/` via + `/tmp/rewrite-imports.py`. Required adding a + `@quarto/preview-renderer` alias to hub-client's three + vitest configs — vitest's exports resolution doesn't honor + the `source` condition the way Vite's prod build does, so + we follow the existing sync-client/automerge-schema + alias convention.)* +- [x] Audit types/utils for editor-only fields. + *(2026-05-13: no splits needed. `project.ts` mentions + "hub-client" in a docstring but the IndexedDB-stored + `ProjectEntry` type is generic enough that the SPA can + consume or ignore it.)* **Acceptance:** - `cd hub-client && npm run typecheck && npm run test:ci && - npm run build:all` passes. -- `cargo xtask verify --skip-rust-tests` passes. -- `npm test --workspace @quarto/preview-renderer` passes. + npm run build:all` passes. ✓ +- `cargo xtask verify --skip-rust-tests` passes. ✓ +- `npm test --workspace @quarto/preview-renderer` passes. ✓ + (81 unit + 7 integration tests including the moved suites.) ### Phase 3 — Move framework/ @@ -610,6 +652,12 @@ internally consistent. - [ ] Move `automergeSync.ts` → `preview-runtime/src/automergeSync.ts`. - [ ] Move `assetWalker.ts` (from `q2-preview/`) → `preview-runtime/src/assetWalker.ts`. +- [ ] Move `iframePostProcessor.ts` (+ `.test.ts`, + `.integration.test.ts`) from hub-client to + `preview-renderer/src/utils/` — deferred from Phase 2 + because it imports `vfsReadFile`/`vfsReadBinaryFile` from + `wasmRenderer`. After this phase the import resolves via + `@quarto/preview-runtime`. - [ ] Move the three `userGrammar*` files → `preview-runtime/src/userGrammar/` (renamed: `Discovery.ts`, `Cache.ts`, `Highlight.ts`). @@ -821,6 +869,14 @@ described. editor. 3. **Audit shared types** for editor-only fields and split as needed. May surface during Phase 2's audit step. +4. **(`bd-hfjj-fu-theme`)** Move `ThemeContext.tsx` to + preview-renderer with DI'd preferences. Currently deferred + because (a) no rendering-side component uses it and (b) the + current implementation hard-codes localStorage-backed + `services/preferences/`. Right shape: `ThemeProvider` takes + `getColorScheme`/`setColorScheme` as props, the SPA passes + no-op or sessionStorage variants. File when the SPA actually + needs theme switching. ## Reference diff --git a/hub-client/src/App.tsx b/hub-client/src/App.tsx index 032e83777..ad1cdca61 100644 --- a/hub-client/src/App.tsx +++ b/hub-client/src/App.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect, useRef, lazy, Suspense } from 'react'; -import type { ProjectEntry, FileEntry } from './types/project'; +import type { ProjectEntry, FileEntry } from '@quarto/preview-renderer/types/project'; import ProjectSelector from './components/ProjectSelector'; import ProjectSetSetup from './components/ProjectSetSetup'; diff --git a/hub-client/src/components/DevHarness.tsx b/hub-client/src/components/DevHarness.tsx index 7bc126d1f..309ba13e0 100644 --- a/hub-client/src/components/DevHarness.tsx +++ b/hub-client/src/components/DevHarness.tsx @@ -10,7 +10,7 @@ import React from 'react'; import ProjectSetSetup from './ProjectSetSetup'; -import type { ProjectEntry } from '../types/project'; +import type { ProjectEntry } from '@quarto/preview-renderer/types/project'; const FAKE_LEGACY_PROJECTS: ProjectEntry[] = [ { diff --git a/hub-client/src/components/Editor.tsx b/hub-client/src/components/Editor.tsx index 43f164b30..58b3028f0 100644 --- a/hub-client/src/components/Editor.tsx +++ b/hub-client/src/components/Editor.tsx @@ -1,8 +1,8 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import MonacoEditor from '@monaco-editor/react'; import type * as Monaco from 'monaco-editor'; -import type { ProjectEntry, FileEntry } from '../types/project'; -import { isBinaryExtension, isTextExtension } from '../types/project'; +import type { ProjectEntry, FileEntry } from '@quarto/preview-renderer/types/project'; +import { isBinaryExtension, isTextExtension } from '@quarto/preview-renderer/types/project'; import type { Route } from '../utils/routing'; import { buildFullUrl, buildShareableUrl } from '../utils/routing'; import { @@ -14,7 +14,7 @@ import { type EditorContentChange, } from '../services/automergeSync'; import { vfsAddFile, isWasmReady } from '../services/wasmRenderer'; -import type { Diagnostic } from '../types/diagnostic'; +import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; import { registerIntelligenceProviders, disposeIntelligenceProviders } from '../services/monacoProviders'; import { processFileForUpload } from '../services/resourceService'; import { usePresence } from '../hooks/usePresence'; diff --git a/hub-client/src/components/FileSidebar.integration.test.tsx b/hub-client/src/components/FileSidebar.integration.test.tsx index 4d24166aa..d0d03f49c 100644 --- a/hub-client/src/components/FileSidebar.integration.test.tsx +++ b/hub-client/src/components/FileSidebar.integration.test.tsx @@ -8,7 +8,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@testing-library/react'; import FileSidebar from './FileSidebar'; -import type { FileEntry } from '../types/project'; +import type { FileEntry } from '@quarto/preview-renderer/types/project'; function file(path: string): FileEntry { return { path, docId: 'doc-' + path }; diff --git a/hub-client/src/components/FileSidebar.tsx b/hub-client/src/components/FileSidebar.tsx index 7e8f7c5c0..793796e34 100644 --- a/hub-client/src/components/FileSidebar.tsx +++ b/hub-client/src/components/FileSidebar.tsx @@ -9,8 +9,8 @@ */ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; -import type { FileEntry } from '../types/project'; -import { isBinaryExtension } from '../types/project'; +import type { FileEntry } from '@quarto/preview-renderer/types/project'; +import { isBinaryExtension } from '@quarto/preview-renderer/types/project'; import { buildFileTree, computeExpandedFolders, diff --git a/hub-client/src/components/OutlinePanel.tsx b/hub-client/src/components/OutlinePanel.tsx index 4d51bfcf5..358b36b6d 100644 --- a/hub-client/src/components/OutlinePanel.tsx +++ b/hub-client/src/components/OutlinePanel.tsx @@ -8,7 +8,7 @@ */ import { useState, useCallback } from 'react'; -import type { Symbol, SymbolKind } from '../types/intelligence'; +import type { Symbol, SymbolKind } from '@quarto/preview-renderer/types/intelligence'; import type { ThumbnailMap } from '../hooks/useSectionThumbnails'; import './OutlinePanel.css'; diff --git a/hub-client/src/components/ProjectSelector.tsx b/hub-client/src/components/ProjectSelector.tsx index 01192be6d..980070eeb 100644 --- a/hub-client/src/components/ProjectSelector.tsx +++ b/hub-client/src/components/ProjectSelector.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { useTheme } from './ThemeContext'; -import type { ProjectEntry } from '../types/project'; +import type { ProjectEntry } from '@quarto/preview-renderer/types/project'; import type { ProjectSetEntry } from '@quarto/quarto-automerge-schema'; import type { ProjectSetStatus } from '../hooks/useProjectSet'; import type { UserSettings } from '../services/storage/types'; diff --git a/hub-client/src/components/ProjectSetSetup.tsx b/hub-client/src/components/ProjectSetSetup.tsx index ca7fefc65..3a4b84d82 100644 --- a/hub-client/src/components/ProjectSetSetup.tsx +++ b/hub-client/src/components/ProjectSetSetup.tsx @@ -7,7 +7,7 @@ */ import { useState, useCallback } from 'react'; -import type { ProjectEntry } from '../types/project'; +import type { ProjectEntry } from '@quarto/preview-renderer/types/project'; import { DEFAULT_SYNC_SERVER } from '../utils/routing'; import { exportData } from '../services/projectStorage'; import './ProjectSetSetup.css'; diff --git a/hub-client/src/components/render/Preview.tsx b/hub-client/src/components/render/Preview.tsx index 0ea7ecbff..f1375d2a2 100644 --- a/hub-client/src/components/render/Preview.tsx +++ b/hub-client/src/components/render/Preview.tsx @@ -1,7 +1,7 @@ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import type * as Monaco from 'monaco-editor'; -import type { FileEntry } from '../../types/project'; -import type { Diagnostic } from '../../types/diagnostic'; +import type { FileEntry } from '@quarto/preview-renderer/types/project'; +import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; import { renderToHtml, isWasmReady, setScrollSyncEnabled, type Pass1Failure } from '../../services/wasmRenderer'; import { getFileContent, getBinaryFileContent } from '../../services/automergeSync'; import { useScrollSync } from '../../hooks/useScrollSync'; diff --git a/hub-client/src/components/render/PreviewErrorOverlay.integration.test.tsx b/hub-client/src/components/render/PreviewErrorOverlay.integration.test.tsx index 806960bbd..d48138485 100644 --- a/hub-client/src/components/render/PreviewErrorOverlay.integration.test.tsx +++ b/hub-client/src/components/render/PreviewErrorOverlay.integration.test.tsx @@ -17,7 +17,7 @@ import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; -import type { Diagnostic } from '../../types/diagnostic'; +import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; let collapsedState = true; const setCollapsedMock = vi.fn((v: boolean) => { diff --git a/hub-client/src/components/render/PreviewErrorOverlay.tsx b/hub-client/src/components/render/PreviewErrorOverlay.tsx index 663c8112f..68c92cfaa 100644 --- a/hub-client/src/components/render/PreviewErrorOverlay.tsx +++ b/hub-client/src/components/render/PreviewErrorOverlay.tsx @@ -1,6 +1,6 @@ -import type { Diagnostic } from '../../types/diagnostic'; +import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; import type { Pass1Failure } from '../../services/wasmRenderer'; -import { stripAnsi } from '../../utils/stripAnsi'; +import { stripAnsi } from '@quarto/preview-renderer/utils/stripAnsi'; import { usePreference } from '../../hooks/usePreference'; interface PreviewErrorOverlayProps { diff --git a/hub-client/src/components/render/PreviewRouter.tsx b/hub-client/src/components/render/PreviewRouter.tsx index 9195fab8c..3b1b5f292 100644 --- a/hub-client/src/components/render/PreviewRouter.tsx +++ b/hub-client/src/components/render/PreviewRouter.tsx @@ -1,8 +1,8 @@ import { useState, useEffect, useRef } from 'react'; import type * as Monaco from 'monaco-editor'; -import type { FileEntry } from '../../types/project'; -import { isQmdFile } from '../../types/project'; -import type { Diagnostic } from '../../types/diagnostic'; +import type { FileEntry } from '@quarto/preview-renderer/types/project'; +import { isQmdFile } from '@quarto/preview-renderer/types/project'; +import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; import { parseQmdToAst, isWasmReady, initWasm } from '../../services/wasmRenderer'; import Preview from './Preview'; import ReactPreview from './ReactPreview'; diff --git a/hub-client/src/components/render/PreviewStaticInfoViews.tsx b/hub-client/src/components/render/PreviewStaticInfoViews.tsx index 9ae7784e4..1e44faaba 100644 --- a/hub-client/src/components/render/PreviewStaticInfoViews.tsx +++ b/hub-client/src/components/render/PreviewStaticInfoViews.tsx @@ -1,5 +1,5 @@ -import type { Diagnostic } from '../../types/diagnostic'; -import { stripAnsi } from '../../utils/stripAnsi'; +import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; +import { stripAnsi } from '@quarto/preview-renderer/utils/stripAnsi'; // Fallback component for when WASM isn't ready yet export function FallbackView({ content, message }: { content: string; message: string }) { diff --git a/hub-client/src/components/render/ReactAstSlideRenderer.tsx b/hub-client/src/components/render/ReactAstSlideRenderer.tsx index e92ea967b..f35c173ea 100644 --- a/hub-client/src/components/render/ReactAstSlideRenderer.tsx +++ b/hub-client/src/components/render/ReactAstSlideRenderer.tsx @@ -3,7 +3,7 @@ import { AspectRatioScaler } from '../render/AspectRatioScaler'; import katex from 'katex'; import 'katex/dist/katex.min.css'; import { vfsReadFile, vfsReadBinaryFile } from '../../services/wasmRenderer'; -import { resolveRelativePath, guessMimeType } from '../../utils/vfsPaths'; +import { resolveRelativePath, guessMimeType } from '@quarto/preview-renderer/utils/vfsPaths'; import type { PandocAST, BlockNode, diff --git a/hub-client/src/components/render/ReactPreview.tsx b/hub-client/src/components/render/ReactPreview.tsx index a76437749..6e31e58cc 100644 --- a/hub-client/src/components/render/ReactPreview.tsx +++ b/hub-client/src/components/render/ReactPreview.tsx @@ -1,10 +1,10 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import type * as Monaco from 'monaco-editor'; -import type { FileEntry } from '../../types/project'; -import type { Diagnostic } from '../../types/diagnostic'; +import type { FileEntry } from '@quarto/preview-renderer/types/project'; +import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; import { parseQmdToAst, renderPageInProject, isWasmReady, incrementalWriteQmd } from '../../services/wasmRenderer'; import { pipelineKindForFormat } from '../../utils/pipelineKind'; -import { stripAnsi } from '../../utils/stripAnsi'; +import { stripAnsi } from '@quarto/preview-renderer/utils/stripAnsi'; import { PreviewErrorOverlay } from './PreviewErrorOverlay'; import ReactRenderer from './ReactRenderer'; diff --git a/hub-client/src/components/render/ReactRenderer.tsx b/hub-client/src/components/render/ReactRenderer.tsx index 5a137f0be..56a2652d2 100644 --- a/hub-client/src/components/render/ReactRenderer.tsx +++ b/hub-client/src/components/render/ReactRenderer.tsx @@ -1,12 +1,12 @@ import { useMemo, Component } from 'react'; import type { ReactNode } from 'react'; -import type { FileEntry } from '../../types/project'; +import type { FileEntry } from '@quarto/preview-renderer/types/project'; import { Q2DebugIframe } from './q2-debug/Q2DebugIframe'; import { Q2PreviewIframe } from './q2-preview/Q2PreviewIframe'; import { SlideAst } from './ReactAstSlideRenderer'; import { RevealjsSlideAst } from './RevealjsReactAstSlideRenderer'; import { transpileTSX } from '../../services/tsxTranspiler'; -import { resolveComponentPath } from '../../utils/componentPath'; +import { resolveComponentPath } from '@quarto/preview-renderer/utils/componentPath'; import type { PandocAST } from './framework/types'; import { extractMetaString } from './framework'; diff --git a/hub-client/src/components/render/framework/Ast.tsx b/hub-client/src/components/render/framework/Ast.tsx index 96e967bc8..3d2243f7b 100644 --- a/hub-client/src/components/render/framework/Ast.tsx +++ b/hub-client/src/components/render/framework/Ast.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { RegistryContext } from './RegistryContext'; import { unwrapCustomNodes } from './customNode'; import type { PandocAST } from './types'; -import type { SourceInfoPool } from '../../../types/sourceInfo'; +import type { SourceInfoPool } from '@quarto/preview-renderer/types/sourceInfo'; interface AstPropsCommon { /** Current file path for resolving relative image paths */ diff --git a/hub-client/src/components/render/framework/RegistryContext.tsx b/hub-client/src/components/render/framework/RegistryContext.tsx index ba35be8ea..a33464cca 100644 --- a/hub-client/src/components/render/framework/RegistryContext.tsx +++ b/hub-client/src/components/render/framework/RegistryContext.tsx @@ -1,5 +1,5 @@ import { createContext } from 'react'; -import type { SourceInfoPool } from '../../../types/sourceInfo'; +import type { SourceInfoPool } from '@quarto/preview-renderer/types/sourceInfo'; /** * Context that carries the active format's registry to the dispatchers, diff --git a/hub-client/src/components/render/framework/dispatch.tsx b/hub-client/src/components/render/framework/dispatch.tsx index 08fbde469..a1e7e30a9 100644 --- a/hub-client/src/components/render/framework/dispatch.tsx +++ b/hub-client/src/components/render/framework/dispatch.tsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import { RegistryContext } from './RegistryContext'; -import { isAtomicSourceInfo, ATOMIC_SYNTHETIC_KINDS } from '../../../utils/sourceInfo'; -import { isAtomicCustomNode } from '../../../utils/atomicCustomNodes'; +import { isAtomicSourceInfo, ATOMIC_SYNTHETIC_KINDS } from '@quarto/preview-renderer/utils/sourceInfo'; +import { isAtomicCustomNode } from '@quarto/preview-renderer/utils/atomicCustomNodes'; import type { BlockNode, InlineNode, diff --git a/hub-client/src/components/render/q2-debug/entry.tsx b/hub-client/src/components/render/q2-debug/entry.tsx index f94fc64a6..af6c88575 100644 --- a/hub-client/src/components/render/q2-debug/entry.tsx +++ b/hub-client/src/components/render/q2-debug/entry.tsx @@ -27,7 +27,7 @@ import { blockStyle, inlineStyle, q2DebugRegistry, } from '.'; -import { buildCustomRegistry, type ComponentExports } from '../../../utils/customRegistry'; +import { buildCustomRegistry, type ComponentExports } from '@quarto/preview-renderer/utils/customRegistry'; // Set the renderer-surface global at module top so importing this // module is sufficient to populate `window.__REACT_AST_DEBUG_RENDERER__` diff --git a/hub-client/src/components/render/q2-preview/Q2PreviewIframe.tsx b/hub-client/src/components/render/q2-preview/Q2PreviewIframe.tsx index 86e60c5f1..ced2c10f7 100644 --- a/hub-client/src/components/render/q2-preview/Q2PreviewIframe.tsx +++ b/hub-client/src/components/render/q2-preview/Q2PreviewIframe.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { vfsReadFile } from '../../../services/wasmRenderer'; -import { DEFAULT_CSS_ARTIFACT_PATH } from '../../../types/artifactPaths'; +import { DEFAULT_CSS_ARTIFACT_PATH } from '@quarto/preview-renderer/types/artifactPaths'; import { buildAssetManifest, type ManifestCacheEntry } from './assetWalker'; interface Q2PreviewIframeProps { diff --git a/hub-client/src/components/render/q2-preview/assetWalker.ts b/hub-client/src/components/render/q2-preview/assetWalker.ts index fbab65eca..3cbf16b9a 100644 --- a/hub-client/src/components/render/q2-preview/assetWalker.ts +++ b/hub-client/src/components/render/q2-preview/assetWalker.ts @@ -19,7 +19,7 @@ */ import { vfsReadBinaryFile } from '../../../services/wasmRenderer'; -import { resolveRelativePath, guessMimeType } from '../../../utils/vfsPaths'; +import { resolveRelativePath, guessMimeType } from '@quarto/preview-renderer/utils/vfsPaths'; export interface ManifestCacheEntry { url: string; diff --git a/hub-client/src/components/render/q2-preview/entry.tsx b/hub-client/src/components/render/q2-preview/entry.tsx index 8b7f30533..21eff5a35 100644 --- a/hub-client/src/components/render/q2-preview/entry.tsx +++ b/hub-client/src/components/render/q2-preview/entry.tsx @@ -51,8 +51,8 @@ import { PreviewTitleBlock } from './custom/PreviewTitleBlock'; import { buildCustomRegistry, type ComponentExports, -} from '../../../utils/customRegistry'; -import { installLinkHandlers } from '../../../utils/iframeLinkHandlers'; +} from '@quarto/preview-renderer/utils/customRegistry'; +import { installLinkHandlers } from '@quarto/preview-renderer/utils/iframeLinkHandlers'; // Set the renderer-surface global at module top. Importing this module // is sufficient to populate `window.__Q2_PREVIEW_RENDERER__`. The diff --git a/hub-client/src/components/tabs/ProjectTab.tsx b/hub-client/src/components/tabs/ProjectTab.tsx index ea1c4691f..24e686781 100644 --- a/hub-client/src/components/tabs/ProjectTab.tsx +++ b/hub-client/src/components/tabs/ProjectTab.tsx @@ -9,7 +9,7 @@ */ import { useState, useCallback } from 'react'; -import type { ProjectEntry } from '../../types/project'; +import type { ProjectEntry } from '@quarto/preview-renderer/types/project'; import './ProjectTab.css'; interface ProjectTabProps { diff --git a/hub-client/src/debug/hooks/useLocalProjects.ts b/hub-client/src/debug/hooks/useLocalProjects.ts index 22142ea0c..2f1dc43a3 100644 --- a/hub-client/src/debug/hooks/useLocalProjects.ts +++ b/hub-client/src/debug/hooks/useLocalProjects.ts @@ -3,7 +3,7 @@ import { listLocalProjects, getLocalProjectSetPointer, } from '../services/localProjects' -import type { ProjectEntry } from '../../types/project' +import type { ProjectEntry } from '@quarto/preview-renderer/types/project' import type { ProjectSetPointer } from '../../services/storage/types' export interface LocalProjectsState { diff --git a/hub-client/src/debug/services/localProjects.ts b/hub-client/src/debug/services/localProjects.ts index 22ffbba1b..a4ac7333c 100644 --- a/hub-client/src/debug/services/localProjects.ts +++ b/hub-client/src/debug/services/localProjects.ts @@ -14,7 +14,7 @@ import { openDB, type IDBPDatabase } from 'idb' import { DB_NAME, STORES, type ProjectSetPointer } from '../../services/storage/types' -import type { ProjectEntry } from '../../types/project' +import type { ProjectEntry } from '@quarto/preview-renderer/types/project' let dbPromise: Promise | null = null diff --git a/hub-client/src/hooks/useAutomergeSync.test.ts b/hub-client/src/hooks/useAutomergeSync.test.ts index a178bdf87..26a319ae6 100644 --- a/hub-client/src/hooks/useAutomergeSync.test.ts +++ b/hub-client/src/hooks/useAutomergeSync.test.ts @@ -21,7 +21,7 @@ vi.mock('../utils/diffToMonacoEdits', () => ({ import { useAutomergeSync } from './useAutomergeSync'; import { getFileContent, setImmediateFileChangeCallback } from '../services/automergeSync'; import { diffToMonacoEdits } from '../utils/diffToMonacoEdits'; -import type { FileEntry } from '../types/project'; +import type { FileEntry } from '@quarto/preview-renderer/types/project'; import { setVisibility, resetVisibility, fireWindowFocus } from '../test-utils/visibility'; const mockGetFileContent = vi.mocked(getFileContent); diff --git a/hub-client/src/hooks/useAutomergeSync.ts b/hub-client/src/hooks/useAutomergeSync.ts index 7ae65c974..4003ccd9f 100644 --- a/hub-client/src/hooks/useAutomergeSync.ts +++ b/hub-client/src/hooks/useAutomergeSync.ts @@ -24,7 +24,7 @@ import { useState, useRef, useEffect, useCallback } from 'react'; import type * as Monaco from 'monaco-editor'; -import type { FileEntry } from '../types/project'; +import type { FileEntry } from '@quarto/preview-renderer/types/project'; import { getFileContent, setImmediateFileChangeCallback, diff --git a/hub-client/src/hooks/useCursorToSlide.ts b/hub-client/src/hooks/useCursorToSlide.ts index 4525edde2..a181b42b9 100644 --- a/hub-client/src/hooks/useCursorToSlide.ts +++ b/hub-client/src/hooks/useCursorToSlide.ts @@ -5,7 +5,7 @@ */ import { useMemo } from 'react'; -import type { Symbol } from '../types/intelligence'; +import type { Symbol } from '@quarto/preview-renderer/types/intelligence'; import { parseSlides } from '../components/render/ReactAstSlideRenderer'; import type { PandocAST } from '../components/render/framework/types'; diff --git a/hub-client/src/hooks/useIntelligence.ts b/hub-client/src/hooks/useIntelligence.ts index 779b2426c..f6af409b0 100644 --- a/hub-client/src/hooks/useIntelligence.ts +++ b/hub-client/src/hooks/useIntelligence.ts @@ -14,7 +14,7 @@ import { type FoldingRange, type DocumentAnalysis, } from '../services/intelligenceService'; -import { isQmdFile } from '../types/project'; +import { isQmdFile } from '@quarto/preview-renderer/types/project'; /** * Options for the useIntelligence hook. diff --git a/hub-client/src/hooks/useProjectSet.ts b/hub-client/src/hooks/useProjectSet.ts index 2bf2f6dab..d24398028 100644 --- a/hub-client/src/hooks/useProjectSet.ts +++ b/hub-client/src/hooks/useProjectSet.ts @@ -17,7 +17,7 @@ import { import * as projectSetService from '../services/projectSetService'; import * as projectStorage from '../services/projectStorage'; import { reconcileIntoConnectedProjectSet } from '../services/projectSetReconciler'; -import type { ProjectEntry } from '../types/project'; +import type { ProjectEntry } from '@quarto/preview-renderer/types/project'; // ============================================================================ // Types diff --git a/hub-client/src/hooks/useSectionThumbnails.ts b/hub-client/src/hooks/useSectionThumbnails.ts index 3effff395..2cb620110 100644 --- a/hub-client/src/hooks/useSectionThumbnails.ts +++ b/hub-client/src/hooks/useSectionThumbnails.ts @@ -7,7 +7,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import html2canvas from 'html2canvas'; -import type { Symbol } from '../types/intelligence'; +import type { Symbol } from '@quarto/preview-renderer/types/intelligence'; /** * Map from symbol line number to thumbnail data URL. diff --git a/hub-client/src/hooks/useSlideThumbnails.tsx b/hub-client/src/hooks/useSlideThumbnails.tsx index 427552526..aa2a7b722 100644 --- a/hub-client/src/hooks/useSlideThumbnails.tsx +++ b/hub-client/src/hooks/useSlideThumbnails.tsx @@ -8,7 +8,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import ReactDOM from 'react-dom/client'; import html2canvas from 'html2canvas'; -import type { Symbol } from '../types/intelligence'; +import type { Symbol } from '@quarto/preview-renderer/types/intelligence'; import { parseSlides, renderSlide } from '../components/render/ReactAstSlideRenderer'; import type { PandocAST } from '../components/render/framework/types'; diff --git a/hub-client/src/services/debugApi.test.ts b/hub-client/src/services/debugApi.test.ts index 923616bb4..031e5bec5 100644 --- a/hub-client/src/services/debugApi.test.ts +++ b/hub-client/src/services/debugApi.test.ts @@ -13,7 +13,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import type { FileEntry } from '@quarto/quarto-automerge-schema'; -import type { ProjectEntry } from '../types/project'; +import type { ProjectEntry } from '@quarto/preview-renderer/types/project'; const automergeSyncMocks = vi.hoisted(() => ({ isFileBinary: vi.fn<(path: string) => boolean>(), diff --git a/hub-client/src/services/debugApi.ts b/hub-client/src/services/debugApi.ts index 19858292c..eb18360fb 100644 --- a/hub-client/src/services/debugApi.ts +++ b/hub-client/src/services/debugApi.ts @@ -16,8 +16,8 @@ * `claude-notes/plans/2026-05-01-hub-client-website-render-ux.md`. */ -import type { ProjectEntry, FileEntry } from '../types/project'; -import { inferMimeType } from '../types/project'; +import type { ProjectEntry, FileEntry } from '@quarto/preview-renderer/types/project'; +import { inferMimeType } from '@quarto/preview-renderer/types/project'; import { getFileContent, getBinaryFileContent, diff --git a/hub-client/src/services/intelligenceService.ts b/hub-client/src/services/intelligenceService.ts index ab4664fef..b367a8d9b 100644 --- a/hub-client/src/services/intelligenceService.ts +++ b/hub-client/src/services/intelligenceService.ts @@ -18,12 +18,12 @@ import type { LspSymbolsResponse, LspFoldingRangesResponse, LspDiagnosticsResponse, -} from '../types/intelligence'; +} from '@quarto/preview-renderer/types/intelligence'; import { initWasm } from './wasmRenderer'; -import { isQmdFile } from '../types/project'; +import { isQmdFile } from '@quarto/preview-renderer/types/project'; // Re-export types for convenience -export type { Symbol, Diagnostic, FoldingRange, DocumentAnalysis } from '../types/intelligence'; +export type { Symbol, Diagnostic, FoldingRange, DocumentAnalysis } from '@quarto/preview-renderer/types/intelligence'; // ============================================================================ // Internal Helpers diff --git a/hub-client/src/services/monacoProviders.ts b/hub-client/src/services/monacoProviders.ts index ef199d379..f36e45b3e 100644 --- a/hub-client/src/services/monacoProviders.ts +++ b/hub-client/src/services/monacoProviders.ts @@ -18,7 +18,7 @@ import { type Symbol, type FoldingRange, } from './intelligenceService'; -import type { SymbolKind, FoldingRangeKind, Range } from '../types/intelligence'; +import type { SymbolKind, FoldingRangeKind, Range } from '@quarto/preview-renderer/types/intelligence'; // ============================================================================ // Type Conversion Utilities diff --git a/hub-client/src/services/projectStorage.ts b/hub-client/src/services/projectStorage.ts index fb3d1c4da..3cda2f3f7 100644 --- a/hub-client/src/services/projectStorage.ts +++ b/hub-client/src/services/projectStorage.ts @@ -4,7 +4,7 @@ * This module provides CRUD operations for project entries and integrates * with the schema versioning/migration system. */ -import type { ProjectEntry } from '../types/project'; +import type { ProjectEntry } from '@quarto/preview-renderer/types/project'; import type { ExportData, UserSettings } from './storage/types'; import { STORES, diff --git a/hub-client/src/services/resourceService.ts b/hub-client/src/services/resourceService.ts index 84ded0ec3..8b4b8e6f3 100644 --- a/hub-client/src/services/resourceService.ts +++ b/hub-client/src/services/resourceService.ts @@ -5,7 +5,7 @@ * Provides SHA-256 hashing, MIME type detection, and conflict-aware naming. */ -import { inferMimeType } from '../types/project'; +import { inferMimeType } from '@quarto/preview-renderer/types/project'; /** * Compute SHA-256 hash of binary data using Web Crypto API. diff --git a/hub-client/src/services/wasmRenderer.ts b/hub-client/src/services/wasmRenderer.ts index dc30ca423..21df7aca0 100644 --- a/hub-client/src/services/wasmRenderer.ts +++ b/hub-client/src/services/wasmRenderer.ts @@ -5,7 +5,7 @@ * VFS operations, QMD rendering, and SASS compilation. */ -import type { Diagnostic, RenderResponse } from '../types/diagnostic'; +import type { Diagnostic, RenderResponse } from '@quarto/preview-renderer/types/diagnostic'; import type { RustQmdJson } from '@quarto/pandoc-types' import type { AstResponse } from 'wasm-quarto-hub-client' import { discoverUserGrammars } from './userGrammarDiscovery'; @@ -21,7 +21,7 @@ interface VfsResponse { } // Re-export Diagnostic type for convenience -export type { Diagnostic } from '../types/diagnostic'; +export type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; // Extended WASM module type with SASS compilation functions interface WasmModuleExtended { diff --git a/hub-client/src/test-utils/mockWasm.ts b/hub-client/src/test-utils/mockWasm.ts index f533f83de..763d97046 100644 --- a/hub-client/src/test-utils/mockWasm.ts +++ b/hub-client/src/test-utils/mockWasm.ts @@ -9,7 +9,7 @@ * - Configurable responses and error injection */ -import type { Diagnostic, RenderResponse } from '../types/diagnostic'; +import type { Diagnostic, RenderResponse } from '@quarto/preview-renderer/types/diagnostic'; /** * VFS response type matching the real wasmRenderer.ts diff --git a/hub-client/src/utils/diagnosticToMonaco.test.ts b/hub-client/src/utils/diagnosticToMonaco.test.ts index 6298266d6..75ca2e7a6 100644 --- a/hub-client/src/utils/diagnosticToMonaco.test.ts +++ b/hub-client/src/utils/diagnosticToMonaco.test.ts @@ -8,8 +8,8 @@ import { lspDiagnosticsToMarkers, type DiagnosticsResult, } from './diagnosticToMonaco'; -import type { Diagnostic as LegacyDiagnostic } from '../types/diagnostic'; -import type { Diagnostic as LspDiagnostic } from '../types/intelligence'; +import type { Diagnostic as LegacyDiagnostic } from '@quarto/preview-renderer/types/diagnostic'; +import type { Diagnostic as LspDiagnostic } from '@quarto/preview-renderer/types/intelligence'; describe('diagnosticsToMarkers (legacy format)', () => { describe('severity conversion', () => { diff --git a/hub-client/src/utils/diagnosticToMonaco.ts b/hub-client/src/utils/diagnosticToMonaco.ts index 660b0bd9b..0d100d408 100644 --- a/hub-client/src/utils/diagnosticToMonaco.ts +++ b/hub-client/src/utils/diagnosticToMonaco.ts @@ -10,14 +10,14 @@ */ import type * as Monaco from 'monaco-editor'; -import type { Diagnostic as LegacyDiagnostic, DiagnosticKind } from '../types/diagnostic'; +import type { Diagnostic as LegacyDiagnostic, DiagnosticKind } from '@quarto/preview-renderer/types/diagnostic'; import type { Diagnostic as LspDiagnostic, DiagnosticSeverity, -} from '../types/intelligence'; +} from '@quarto/preview-renderer/types/intelligence'; // Re-export legacy type with alias for backwards compatibility -export type { Diagnostic as LegacyDiagnostic } from '../types/diagnostic'; +export type { Diagnostic as LegacyDiagnostic } from '@quarto/preview-renderer/types/diagnostic'; /** * Result of converting diagnostics to Monaco markers. diff --git a/hub-client/src/utils/fileTree.ts b/hub-client/src/utils/fileTree.ts index 6ac669e9a..c0139380a 100644 --- a/hub-client/src/utils/fileTree.ts +++ b/hub-client/src/utils/fileTree.ts @@ -6,7 +6,7 @@ * a collapsible folder hierarchy. */ -import type { FileEntry } from '../types/project'; +import type { FileEntry } from '@quarto/preview-renderer/types/project'; /** * A node in the file tree, representing either a folder or a file. diff --git a/hub-client/src/utils/iframePostProcessor.ts b/hub-client/src/utils/iframePostProcessor.ts index d07adad5b..9c08e8767 100644 --- a/hub-client/src/utils/iframePostProcessor.ts +++ b/hub-client/src/utils/iframePostProcessor.ts @@ -10,7 +10,7 @@ */ import { vfsReadFile, vfsReadBinaryFile } from '../services/wasmRenderer'; -import { resolveRelativePath, guessMimeType } from './vfsPaths'; +import { resolveRelativePath, guessMimeType } from '@quarto/preview-renderer/utils/vfsPaths'; /** * VFS path under which the website renderer flushes its diff --git a/hub-client/vitest.config.ts b/hub-client/vitest.config.ts index 5d7a5f81f..c6c00a367 100644 --- a/hub-client/vitest.config.ts +++ b/hub-client/vitest.config.ts @@ -11,6 +11,10 @@ export default mergeConfig( alias: { '@quarto/quarto-automerge-schema': path.resolve(__dirname, '../ts-packages/quarto-automerge-schema/src/index.ts'), '@quarto/quarto-sync-client': path.resolve(__dirname, '../ts-packages/quarto-sync-client/src/index.ts'), + // Sub-path aware: preview-renderer exposes types/* and utils/*. + // Aliasing to the src dir lets `@quarto/preview-renderer/utils/` + // resolve to `/utils/.ts` via Vite's default extension list. + '@quarto/preview-renderer': path.resolve(__dirname, '../ts-packages/preview-renderer/src'), }, }, test: { diff --git a/hub-client/vitest.integration.config.ts b/hub-client/vitest.integration.config.ts index 25c47f7df..56fee6db2 100644 --- a/hub-client/vitest.integration.config.ts +++ b/hub-client/vitest.integration.config.ts @@ -11,6 +11,7 @@ export default mergeConfig( alias: { '@quarto/quarto-automerge-schema': path.resolve(__dirname, '../ts-packages/quarto-automerge-schema/src/index.ts'), '@quarto/quarto-sync-client': path.resolve(__dirname, '../ts-packages/quarto-sync-client/src/index.ts'), + '@quarto/preview-renderer': path.resolve(__dirname, '../ts-packages/preview-renderer/src'), }, }, test: { diff --git a/hub-client/vitest.wasm.config.ts b/hub-client/vitest.wasm.config.ts index 7867868bf..1f3517360 100644 --- a/hub-client/vitest.wasm.config.ts +++ b/hub-client/vitest.wasm.config.ts @@ -26,6 +26,7 @@ export default mergeConfig( // The WASM JS file imports from `/src/...` which only works in vite dev server. // Map it to the actual source directory for tests. '/src': path.resolve(__dirname, 'src'), + '@quarto/preview-renderer': path.resolve(__dirname, '../ts-packages/preview-renderer/src'), }, }, }), diff --git a/ts-packages/preview-renderer/package.json b/ts-packages/preview-renderer/package.json index 33b959309..85d1fba35 100644 --- a/ts-packages/preview-renderer/package.json +++ b/ts-packages/preview-renderer/package.json @@ -15,6 +15,16 @@ "types": "./src/index.ts", "source": "./src/index.ts", "import": "./dist/index.js" + }, + "./types/*": { + "types": "./src/types/*.ts", + "source": "./src/types/*.ts", + "import": "./dist/types/*.js" + }, + "./utils/*": { + "types": "./src/utils/*.ts", + "source": "./src/utils/*.ts", + "import": "./dist/utils/*.js" } }, "files": [ @@ -29,6 +39,7 @@ "test:watch": "vitest" }, "dependencies": { + "@quarto/quarto-automerge-schema": "*", "morphdom": "^2.7.8", "react": "^19.2.0", "react-dom": "^19.2.0" diff --git a/ts-packages/preview-renderer/src/index.ts b/ts-packages/preview-renderer/src/index.ts index cb0ff5c3b..d1a808818 100644 --- a/ts-packages/preview-renderer/src/index.ts +++ b/ts-packages/preview-renderer/src/index.ts @@ -1 +1,12 @@ +// Most types/utils are consumed via sub-path imports +// (`@quarto/preview-renderer/types/`, +// `@quarto/preview-renderer/utils/`), declared in +// `package.json`'s `exports` map. This keeps the API surface +// stable as files are added, and avoids name collisions between +// modules that legitimately export the same identifier (the +// deprecated `Diagnostic` in `types/diagnostic` vs the LSP-style +// `Diagnostic` in `types/intelligence`). +// +// Top-level barrel exports are added here as Phases 3–4 move +// components whose public-API names don't collide. export {}; diff --git a/hub-client/src/types/artifactPaths.ts b/ts-packages/preview-renderer/src/types/artifactPaths.ts similarity index 100% rename from hub-client/src/types/artifactPaths.ts rename to ts-packages/preview-renderer/src/types/artifactPaths.ts diff --git a/hub-client/src/types/diagnostic.ts b/ts-packages/preview-renderer/src/types/diagnostic.ts similarity index 100% rename from hub-client/src/types/diagnostic.ts rename to ts-packages/preview-renderer/src/types/diagnostic.ts diff --git a/hub-client/src/types/intelligence.ts b/ts-packages/preview-renderer/src/types/intelligence.ts similarity index 100% rename from hub-client/src/types/intelligence.ts rename to ts-packages/preview-renderer/src/types/intelligence.ts diff --git a/hub-client/src/types/project.test.ts b/ts-packages/preview-renderer/src/types/project.test.ts similarity index 100% rename from hub-client/src/types/project.test.ts rename to ts-packages/preview-renderer/src/types/project.test.ts diff --git a/hub-client/src/types/project.ts b/ts-packages/preview-renderer/src/types/project.ts similarity index 100% rename from hub-client/src/types/project.ts rename to ts-packages/preview-renderer/src/types/project.ts diff --git a/hub-client/src/types/sourceInfo.ts b/ts-packages/preview-renderer/src/types/sourceInfo.ts similarity index 100% rename from hub-client/src/types/sourceInfo.ts rename to ts-packages/preview-renderer/src/types/sourceInfo.ts diff --git a/hub-client/src/utils/atomicCustomNodes.ts b/ts-packages/preview-renderer/src/utils/atomicCustomNodes.ts similarity index 100% rename from hub-client/src/utils/atomicCustomNodes.ts rename to ts-packages/preview-renderer/src/utils/atomicCustomNodes.ts diff --git a/hub-client/src/utils/componentPath.test.ts b/ts-packages/preview-renderer/src/utils/componentPath.test.ts similarity index 100% rename from hub-client/src/utils/componentPath.test.ts rename to ts-packages/preview-renderer/src/utils/componentPath.test.ts diff --git a/hub-client/src/utils/componentPath.ts b/ts-packages/preview-renderer/src/utils/componentPath.ts similarity index 100% rename from hub-client/src/utils/componentPath.ts rename to ts-packages/preview-renderer/src/utils/componentPath.ts diff --git a/hub-client/src/utils/customRegistry.test.ts b/ts-packages/preview-renderer/src/utils/customRegistry.test.ts similarity index 100% rename from hub-client/src/utils/customRegistry.test.ts rename to ts-packages/preview-renderer/src/utils/customRegistry.test.ts diff --git a/hub-client/src/utils/customRegistry.ts b/ts-packages/preview-renderer/src/utils/customRegistry.ts similarity index 100% rename from hub-client/src/utils/customRegistry.ts rename to ts-packages/preview-renderer/src/utils/customRegistry.ts diff --git a/hub-client/src/utils/iframeLinkHandlers.integration.test.ts b/ts-packages/preview-renderer/src/utils/iframeLinkHandlers.integration.test.ts similarity index 100% rename from hub-client/src/utils/iframeLinkHandlers.integration.test.ts rename to ts-packages/preview-renderer/src/utils/iframeLinkHandlers.integration.test.ts diff --git a/hub-client/src/utils/iframeLinkHandlers.ts b/ts-packages/preview-renderer/src/utils/iframeLinkHandlers.ts similarity index 100% rename from hub-client/src/utils/iframeLinkHandlers.ts rename to ts-packages/preview-renderer/src/utils/iframeLinkHandlers.ts diff --git a/hub-client/src/utils/sourceInfo.test.ts b/ts-packages/preview-renderer/src/utils/sourceInfo.test.ts similarity index 100% rename from hub-client/src/utils/sourceInfo.test.ts rename to ts-packages/preview-renderer/src/utils/sourceInfo.test.ts diff --git a/hub-client/src/utils/sourceInfo.ts b/ts-packages/preview-renderer/src/utils/sourceInfo.ts similarity index 100% rename from hub-client/src/utils/sourceInfo.ts rename to ts-packages/preview-renderer/src/utils/sourceInfo.ts diff --git a/hub-client/src/utils/stripAnsi.test.ts b/ts-packages/preview-renderer/src/utils/stripAnsi.test.ts similarity index 100% rename from hub-client/src/utils/stripAnsi.test.ts rename to ts-packages/preview-renderer/src/utils/stripAnsi.test.ts diff --git a/hub-client/src/utils/stripAnsi.ts b/ts-packages/preview-renderer/src/utils/stripAnsi.ts similarity index 100% rename from hub-client/src/utils/stripAnsi.ts rename to ts-packages/preview-renderer/src/utils/stripAnsi.ts diff --git a/hub-client/src/utils/vfsPaths.test.ts b/ts-packages/preview-renderer/src/utils/vfsPaths.test.ts similarity index 100% rename from hub-client/src/utils/vfsPaths.test.ts rename to ts-packages/preview-renderer/src/utils/vfsPaths.test.ts diff --git a/hub-client/src/utils/vfsPaths.ts b/ts-packages/preview-renderer/src/utils/vfsPaths.ts similarity index 100% rename from hub-client/src/utils/vfsPaths.ts rename to ts-packages/preview-renderer/src/utils/vfsPaths.ts diff --git a/ts-packages/preview-renderer/vitest.config.ts b/ts-packages/preview-renderer/vitest.config.ts index c149fe531..f1e09f029 100644 --- a/ts-packages/preview-renderer/vitest.config.ts +++ b/ts-packages/preview-renderer/vitest.config.ts @@ -1,8 +1,17 @@ import { defineConfig } from 'vitest/config'; +import path from 'path'; export default defineConfig({ resolve: { conditions: ['source', 'import', 'module', 'browser', 'default'], + alias: { + // Resolve workspace packages to source so tests work on a fresh + // clone (no built dist required). Mirrors hub-client/vitest.config.ts. + '@quarto/quarto-automerge-schema': path.resolve( + __dirname, + '../quarto-automerge-schema/src/index.ts', + ), + }, }, test: { environment: 'node', diff --git a/ts-packages/preview-renderer/vitest.integration.config.ts b/ts-packages/preview-renderer/vitest.integration.config.ts index 08007a822..a7e2e3012 100644 --- a/ts-packages/preview-renderer/vitest.integration.config.ts +++ b/ts-packages/preview-renderer/vitest.integration.config.ts @@ -1,8 +1,15 @@ import { defineConfig } from 'vitest/config'; +import path from 'path'; export default defineConfig({ resolve: { conditions: ['source', 'import', 'module', 'browser', 'default'], + alias: { + '@quarto/quarto-automerge-schema': path.resolve( + __dirname, + '../quarto-automerge-schema/src/index.ts', + ), + }, }, test: { environment: 'jsdom', From 72a67e772276f04c5e5e233b71ef8862e398786a Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 10:16:19 -0500 Subject: [PATCH 005/108] docs(hub-client): changelog entry for bd-hfjj Phase 2 Co-Authored-By: Claude Opus 4.7 (1M context) --- hub-client/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hub-client/changelog.md b/hub-client/changelog.md index 2b0415ef7..434e5fa72 100644 --- a/hub-client/changelog.md +++ b/hub-client/changelog.md @@ -13,6 +13,10 @@ be in reverse chronological order (latest first). --> +### 2026-05-13 + +- [`5d8bd2b3`](https://github.com/quarto-dev/q2/commits/5d8bd2b3): Internal — start carving the preview pane into shared workspace packages (bd-hfjj Phase 2). Five type modules and seven utilities move from `hub-client/src/` into the new `@quarto/preview-renderer` package; hub-client now imports them via sub-paths. No user-visible change; this prepares for the `q2 preview` SPA reusing the exact same rendering code. + ### 2026-05-10 - [`68e5ec24`](https://github.com/quarto-dev/q2/commits/68e5ec24): Custom syntax highlighting now works in Quarto Hub projects (bd-izfv) — user-supplied tree-sitter grammars under `_quarto/grammars//` now apply to code blocks even when the qmd file lives under a `_quarto.yml` ancestor, matching the single-file render path. From 7e45d2bf9d1293d1e76207a8d2a4c4f573a73d97 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 10:22:27 -0500 Subject: [PATCH 006/108] refactor(q2-preview): move framework subtree to preview-renderer (bd-hfjj Phase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the entire components/render/framework/ subtree out of hub-client into @quarto/preview-renderer: ts-packages/preview-renderer/src/framework/ Ast.tsx, RegistryContext.tsx, dispatch.tsx, customNode.ts (+ test), meta.ts (+ test), plainText.ts (+ test), types.ts, index.ts framework is exposed as a single barrel via `./framework` in package.json's exports map (not a wildcard sub-path). Rationale: the subtree mixes .tsx and .ts files, and Node's exports map can't pattern-match both extensions in a single wildcard entry cleanly. Every symbol re-exports through framework/index.ts already, so consumers don't need direct sub-file imports. 107 imports across 60 hub-client files rewritten to `from '@quarto/preview-renderer/framework'`. One dynamic `await import('./framework')` in parity.integration.test.tsx also caught and rewritten. Framework's internal cross-dir imports — Phase 2 had rewritten Ast/RegistryContext/dispatch to use self-package `@quarto/preview-renderer/...` paths since their targets had moved already — are converted to idiomatic relative paths (`../types/sourceInfo`, `../utils/sourceInfo`, etc.) now that framework itself lives inside the package. preview-renderer's test suite now exercises 133 tests across 10 files (adds customNode/meta/plainText tests). hub-client test:ci, build:all, and `cargo xtask verify --skip-rust-tests` all green. Snapshot tests: no snapshot files changed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-11-hub-client-decomposition.md | 33 +++++++++++++++---- .../render/ReactAstSlideRenderer.test.tsx | 2 +- .../render/ReactAstSlideRenderer.tsx | 4 +-- .../src/components/render/ReactRenderer.tsx | 4 +-- .../render/RevealjsReactAstSlideRenderer.tsx | 2 +- .../src/components/render/getQ2Format.ts | 2 +- .../render/parity.integration.test.tsx | 2 +- .../components/render/q2-debug/components.tsx | 4 +-- .../render/q2-debug/dispatchers.tsx | 4 +-- .../src/components/render/q2-debug/entry.tsx | 4 +-- .../q2-debug/q2-debug.integration.test.tsx | 4 +-- .../components/render/q2-debug/registry.ts | 2 +- .../q2-preview/NoteNumberingContext.tsx | 2 +- .../PreviewDocument.integration.test.tsx | 4 +-- .../render/q2-preview/PreviewDocument.tsx | 4 +-- .../render/q2-preview/blocks/BlockQuote.tsx | 4 +-- .../render/q2-preview/blocks/BulletList.tsx | 4 +-- .../render/q2-preview/blocks/CodeBlock.tsx | 2 +- .../q2-preview/blocks/DefinitionList.tsx | 4 +-- .../render/q2-preview/blocks/Div.tsx | 4 +-- .../render/q2-preview/blocks/Figure.tsx | 4 +-- .../render/q2-preview/blocks/Header.tsx | 4 +-- .../render/q2-preview/blocks/LineBlock.tsx | 4 +-- .../render/q2-preview/blocks/OrderedList.tsx | 4 +-- .../render/q2-preview/blocks/Para.tsx | 4 +-- .../render/q2-preview/blocks/Plain.tsx | 4 +-- .../render/q2-preview/blocks/RawBlock.tsx | 2 +- .../render/q2-preview/blocks/Table.tsx | 4 +-- .../custom-components.integration.test.tsx | 4 +-- .../render/q2-preview/custom/Callout.tsx | 2 +- .../q2-preview/custom/CrossrefResolvedRef.tsx | 4 +-- .../render/q2-preview/custom/Equation.tsx | 4 +-- .../render/q2-preview/custom/Fallback.tsx | 4 +-- .../q2-preview/custom/FloatRefTarget.tsx | 4 +-- .../PreviewTitleBlock.integration.test.tsx | 4 +-- .../q2-preview/custom/PreviewTitleBlock.tsx | 4 +-- .../render/q2-preview/custom/Proof.tsx | 4 +-- .../render/q2-preview/custom/Theorem.tsx | 4 +-- .../render/q2-preview/dispatchers.tsx | 6 ++-- .../components/render/q2-preview/entry.tsx | 4 +-- .../render/q2-preview/inlines/Cite.tsx | 4 +-- .../render/q2-preview/inlines/Code.tsx | 2 +- .../render/q2-preview/inlines/Emph.tsx | 4 +-- .../render/q2-preview/inlines/Image.tsx | 4 +-- .../render/q2-preview/inlines/Link.tsx | 4 +-- .../render/q2-preview/inlines/Math.tsx | 2 +- .../render/q2-preview/inlines/Note.tsx | 4 +-- .../render/q2-preview/inlines/Quoted.tsx | 4 +-- .../render/q2-preview/inlines/RawInline.tsx | 2 +- .../render/q2-preview/inlines/SmallCaps.tsx | 4 +-- .../render/q2-preview/inlines/Span.tsx | 4 +-- .../render/q2-preview/inlines/Str.tsx | 2 +- .../render/q2-preview/inlines/Strikeout.tsx | 4 +-- .../render/q2-preview/inlines/Strong.tsx | 4 +-- .../render/q2-preview/inlines/Subscript.tsx | 4 +-- .../render/q2-preview/inlines/Superscript.tsx | 4 +-- .../render/q2-preview/inlines/Underline.tsx | 4 +-- .../q2-preview.integration.test.tsx | 4 +-- .../components/render/q2-preview/registry.ts | 2 +- .../components/render/q2-preview/utils.tsx | 6 ++-- hub-client/src/hooks/useCursorToSlide.ts | 2 +- hub-client/src/hooks/useSlideThumbnails.tsx | 2 +- ts-packages/preview-renderer/package.json | 5 +++ .../preview-renderer/src}/framework/Ast.tsx | 2 +- .../src}/framework/RegistryContext.tsx | 2 +- .../src}/framework/customNode.test.ts | 0 .../src}/framework/customNode.ts | 0 .../src}/framework/dispatch.tsx | 4 +-- .../preview-renderer/src}/framework/index.ts | 0 .../src}/framework/meta.test.ts | 0 .../preview-renderer/src}/framework/meta.ts | 0 .../src}/framework/plainText.test.ts | 0 .../src}/framework/plainText.ts | 0 .../preview-renderer/src}/framework/types.ts | 0 74 files changed, 143 insertions(+), 119 deletions(-) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/Ast.tsx (97%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/RegistryContext.tsx (92%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/customNode.test.ts (100%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/customNode.ts (100%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/dispatch.tsx (99%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/index.ts (100%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/meta.test.ts (100%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/meta.ts (100%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/plainText.test.ts (100%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/plainText.ts (100%) rename {hub-client/src/components/render => ts-packages/preview-renderer/src}/framework/types.ts (100%) diff --git a/claude-notes/plans/2026-05-11-hub-client-decomposition.md b/claude-notes/plans/2026-05-11-hub-client-decomposition.md index 6ff8a90f3..fdb8875d6 100644 --- a/claude-notes/plans/2026-05-11-hub-client-decomposition.md +++ b/claude-notes/plans/2026-05-11-hub-client-decomposition.md @@ -3,7 +3,7 @@ date: 2026-05-11 updated: 2026-05-13 branch: beads/bd-hfjj-hub-client-decomposition-shared beads: bd-hfjj (sub-epic of bd-kw93) -status: approved 2026-05-13; Phases 0–2 complete; Phase 3 next +status: approved 2026-05-13; Phases 0–3 complete; Phase 4 next --- # Hub-client decomposition: shared preview-pane packages for hub-client + q2-preview-spa @@ -594,15 +594,34 @@ The lowest-risk moves. Pure data + pure functions; no React tree. ### Phase 3 — Move framework/ -- [ ] Move `components/render/framework/` entire subtree to +- [x] Move `components/render/framework/` entire subtree to `ts-packages/preview-renderer/src/framework/`. -- [ ] Re-export through `src/index.ts`. -- [ ] Update import paths in everything that imports from - `components/render/framework/...`. Confirm that the framework - tests (`framework/*.test.ts`) run via + *(2026-05-13.)* +- [x] Expose as a single barrel via `package.json` exports + `./framework`. Wildcards rejected because the subtree mixes + `.tsx` (Ast, dispatch, RegistryContext) and `.ts` (the rest) + and Node's exports map can't pattern-match both extensions + cleanly. Every framework symbol re-exports through + `framework/index.ts`, so sub-file imports are not needed. +- [x] Update import paths in everything that imports from + `components/render/framework/...`. + *(2026-05-13: 107 imports across 60 hub-client files + rewritten to `from '@quarto/preview-renderer/framework'`. + Also caught one dynamic `await import('./framework')` in + `parity.integration.test.tsx`.)* +- [x] Convert framework's internal cross-dir imports (Phase 2 + Phase-2-style `@quarto/preview-renderer/...` self-imports + from Ast/RegistryContext/dispatch) to relative paths + (`../types/sourceInfo`, `../utils/sourceInfo`, etc.). + Self-package imports work but are unidiomatic. +- [x] Framework tests run via `npm test --workspace @quarto/preview-renderer`. + *(2026-05-13: 133 tests / 10 files including the moved + `customNode.test.ts`, `meta.test.ts`, `plainText.test.ts`.)* -**Acceptance:** same as Phase 2. +**Acceptance:** same as Phase 2. ✓ +- hub-client `typecheck`, `test:ci`, `build:all` all green. +- `cargo xtask verify --skip-rust-tests` green. ### Phase 4 — Move q2-preview/, iframe wrappers, overlays diff --git a/hub-client/src/components/render/ReactAstSlideRenderer.test.tsx b/hub-client/src/components/render/ReactAstSlideRenderer.test.tsx index 943b44cb7..b141d2549 100644 --- a/hub-client/src/components/render/ReactAstSlideRenderer.test.tsx +++ b/hub-client/src/components/render/ReactAstSlideRenderer.test.tsx @@ -15,7 +15,7 @@ */ import { describe, expect, it } from 'vitest'; -import type { PandocAST } from './framework'; +import type { PandocAST } from '@quarto/preview-renderer/framework'; import { parseSlides } from './ReactAstSlideRenderer'; describe('ReactAstSlideRenderer slide-title meta coercion', () => { diff --git a/hub-client/src/components/render/ReactAstSlideRenderer.tsx b/hub-client/src/components/render/ReactAstSlideRenderer.tsx index f35c173ea..c919fb8ee 100644 --- a/hub-client/src/components/render/ReactAstSlideRenderer.tsx +++ b/hub-client/src/components/render/ReactAstSlideRenderer.tsx @@ -27,8 +27,8 @@ import type { SpanInline, MathInline, QuotedInline, -} from './framework/types'; -import { extractMetaString } from './framework'; +} from '@quarto/preview-renderer/framework'; +import { extractMetaString } from '@quarto/preview-renderer/framework'; /** * Represents a single slide with its content diff --git a/hub-client/src/components/render/ReactRenderer.tsx b/hub-client/src/components/render/ReactRenderer.tsx index 56a2652d2..6ce535a88 100644 --- a/hub-client/src/components/render/ReactRenderer.tsx +++ b/hub-client/src/components/render/ReactRenderer.tsx @@ -7,8 +7,8 @@ import { SlideAst } from './ReactAstSlideRenderer'; import { RevealjsSlideAst } from './RevealjsReactAstSlideRenderer'; import { transpileTSX } from '../../services/tsxTranspiler'; import { resolveComponentPath } from '@quarto/preview-renderer/utils/componentPath'; -import type { PandocAST } from './framework/types'; -import { extractMetaString } from './framework'; +import type { PandocAST } from '@quarto/preview-renderer/framework'; +import { extractMetaString } from '@quarto/preview-renderer/framework'; // Simple error boundary to catch errors in custom components class ErrorBoundary extends Component< diff --git a/hub-client/src/components/render/RevealjsReactAstSlideRenderer.tsx b/hub-client/src/components/render/RevealjsReactAstSlideRenderer.tsx index c6d38acb6..2a1e8501c 100644 --- a/hub-client/src/components/render/RevealjsReactAstSlideRenderer.tsx +++ b/hub-client/src/components/render/RevealjsReactAstSlideRenderer.tsx @@ -11,7 +11,7 @@ import RevealZoom from 'reveal.js/plugin/zoom'; import RevealMenuPlugin from 'reveal.js-menu/plugin.js'; const RevealMenu = RevealMenuPlugin.default || RevealMenuPlugin; import { parseSlides, renderBlock, type Slide as PandocSlide } from './ReactAstSlideRenderer'; -import type { PandocAST } from './framework/types'; +import type { PandocAST } from '@quarto/preview-renderer/framework'; interface RevealjsSlideRendererProps { astJson: string; diff --git a/hub-client/src/components/render/getQ2Format.ts b/hub-client/src/components/render/getQ2Format.ts index 5347cf24a..48b43ec14 100644 --- a/hub-client/src/components/render/getQ2Format.ts +++ b/hub-client/src/components/render/getQ2Format.ts @@ -1,4 +1,4 @@ -import { extractMetaString } from './framework'; +import { extractMetaString } from '@quarto/preview-renderer/framework'; /** * Extract format string from the parsed AST metadata. diff --git a/hub-client/src/components/render/parity.integration.test.tsx b/hub-client/src/components/render/parity.integration.test.tsx index fa09ed0ae..cc38292d5 100644 --- a/hub-client/src/components/render/parity.integration.test.tsx +++ b/hub-client/src/components/render/parity.integration.test.tsx @@ -53,7 +53,7 @@ describe('framework-primitive parity across iframe globals', () => { test.each(FRAMEWORK_PRIMITIVES)( '%s is reference-equal across both globals and the framework module', async (name) => { - const framework = await import('./framework'); + const framework = await import('@quarto/preview-renderer/framework'); const debug = (window as any).__REACT_AST_DEBUG_RENDERER__[name]; const preview = (window as any).__Q2_PREVIEW_RENDERER__[name]; expect(debug).toBe((framework as any)[name]); diff --git a/hub-client/src/components/render/q2-debug/components.tsx b/hub-client/src/components/render/q2-debug/components.tsx index 8e54de137..2aea0054a 100644 --- a/hub-client/src/components/render/q2-debug/components.tsx +++ b/hub-client/src/components/render/q2-debug/components.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Node, renderChildren } from '../framework/dispatch'; +import { Node, renderChildren } from '@quarto/preview-renderer/framework'; import type { InlineNode, NodeArgs, @@ -26,7 +26,7 @@ import type { ImageInline, SpanInline, QuotedInline, -} from '../framework/types'; +} from '@quarto/preview-renderer/framework'; import { blockStyle, inlineStyle } from './styles'; export const Para = (args: NodeArgs) => ( diff --git a/hub-client/src/components/render/q2-debug/dispatchers.tsx b/hub-client/src/components/render/q2-debug/dispatchers.tsx index 30a141624..c34fb99e6 100644 --- a/hub-client/src/components/render/q2-debug/dispatchers.tsx +++ b/hub-client/src/components/render/q2-debug/dispatchers.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; -import { RegistryContext } from '../framework/RegistryContext'; -import type { BlockNode, InlineNode, NodeArgs } from '../framework/types'; +import { RegistryContext } from '@quarto/preview-renderer/framework'; +import type { BlockNode, InlineNode, NodeArgs } from '@quarto/preview-renderer/framework'; import { blockStyle, inlineStyle } from './styles'; /** diff --git a/hub-client/src/components/render/q2-debug/entry.tsx b/hub-client/src/components/render/q2-debug/entry.tsx index af6c88575..cfb2568f7 100644 --- a/hub-client/src/components/render/q2-debug/entry.tsx +++ b/hub-client/src/components/render/q2-debug/entry.tsx @@ -15,8 +15,8 @@ import 'reveal.js/theme/white.css'; import katex from 'katex'; import 'katex/dist/katex.min.css'; -import { Ast, Node, renderChildren, renderNode } from '../framework'; -import type { FormatRegistry } from '../framework'; +import { Ast, Node, renderChildren, renderNode } from '@quarto/preview-renderer/framework'; +import type { FormatRegistry } from '@quarto/preview-renderer/framework'; import { Block, Inline, diff --git a/hub-client/src/components/render/q2-debug/q2-debug.integration.test.tsx b/hub-client/src/components/render/q2-debug/q2-debug.integration.test.tsx index 7ddb62dc8..5c84a5941 100644 --- a/hub-client/src/components/render/q2-debug/q2-debug.integration.test.tsx +++ b/hub-client/src/components/render/q2-debug/q2-debug.integration.test.tsx @@ -28,8 +28,8 @@ import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/react'; -import { Ast } from '../framework'; -import type { PandocAST } from '../framework'; +import { Ast } from '@quarto/preview-renderer/framework'; +import type { PandocAST } from '@quarto/preview-renderer/framework'; import { q2DebugRegistry } from './registry'; function astJson(blocks: any[], meta: Record = {}): string { diff --git a/hub-client/src/components/render/q2-debug/registry.ts b/hub-client/src/components/render/q2-debug/registry.ts index d5f1fdf0a..3597e4607 100644 --- a/hub-client/src/components/render/q2-debug/registry.ts +++ b/hub-client/src/components/render/q2-debug/registry.ts @@ -1,4 +1,4 @@ -import type { FormatRegistry } from '../framework/types'; +import type { FormatRegistry } from '@quarto/preview-renderer/framework'; import { Block, Inline } from './dispatchers'; import { BlockComponents, diff --git a/hub-client/src/components/render/q2-preview/NoteNumberingContext.tsx b/hub-client/src/components/render/q2-preview/NoteNumberingContext.tsx index 35168421e..7011e99a7 100644 --- a/hub-client/src/components/render/q2-preview/NoteNumberingContext.tsx +++ b/hub-client/src/components/render/q2-preview/NoteNumberingContext.tsx @@ -1,5 +1,5 @@ import { createContext } from 'react'; -import type { NoteInline } from '../framework'; +import type { NoteInline } from '@quarto/preview-renderer/framework'; /** * Distributes JS-side note numbering to `Note.tsx`. diff --git a/hub-client/src/components/render/q2-preview/PreviewDocument.integration.test.tsx b/hub-client/src/components/render/q2-preview/PreviewDocument.integration.test.tsx index 2e5e9c650..78b63f06c 100644 --- a/hub-client/src/components/render/q2-preview/PreviewDocument.integration.test.tsx +++ b/hub-client/src/components/render/q2-preview/PreviewDocument.integration.test.tsx @@ -10,8 +10,8 @@ import { describe, expect, it, beforeEach, afterEach } from 'vitest'; import { render } from '@testing-library/react'; -import { Ast } from '../framework'; -import type { PandocAST } from '../framework'; +import { Ast } from '@quarto/preview-renderer/framework'; +import type { PandocAST } from '@quarto/preview-renderer/framework'; import { previewRegistry } from './registry'; function astJson(meta: Record, blocks: any[] = []): string { diff --git a/hub-client/src/components/render/q2-preview/PreviewDocument.tsx b/hub-client/src/components/render/q2-preview/PreviewDocument.tsx index c1de1d183..b876e09a1 100644 --- a/hub-client/src/components/render/q2-preview/PreviewDocument.tsx +++ b/hub-client/src/components/render/q2-preview/PreviewDocument.tsx @@ -4,8 +4,8 @@ import { extractMetaString, extractMetaBool, RegistryContext, -} from '../framework'; -import type { BlockNode, PandocAST } from '../framework'; +} from '@quarto/preview-renderer/framework'; +import type { BlockNode, PandocAST } from '@quarto/preview-renderer/framework'; import * as Custom from './custom'; /** diff --git a/hub-client/src/components/render/q2-preview/blocks/BlockQuote.tsx b/hub-client/src/components/render/q2-preview/blocks/BlockQuote.tsx index e20f0134e..a87e56deb 100644 --- a/hub-client/src/components/render/q2-preview/blocks/BlockQuote.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/BlockQuote.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { BlockQuoteBlock, NodeArgs } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { BlockQuoteBlock, NodeArgs } from '@quarto/preview-renderer/framework'; export const BlockQuote = (args: NodeArgs) => (
{renderChildren(args)}
diff --git a/hub-client/src/components/render/q2-preview/blocks/BulletList.tsx b/hub-client/src/components/render/q2-preview/blocks/BulletList.tsx index 7dfe58fa1..bdecc78b8 100644 --- a/hub-client/src/components/render/q2-preview/blocks/BulletList.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/BulletList.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { BulletListBlock, NodeArgs } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { BulletListBlock, NodeArgs } from '@quarto/preview-renderer/framework'; /** BulletList →
    . The framework's `renderChildrenRegistry.BulletList` * already wraps each item array in an
  • ; the component just supplies diff --git a/hub-client/src/components/render/q2-preview/blocks/CodeBlock.tsx b/hub-client/src/components/render/q2-preview/blocks/CodeBlock.tsx index cdc9a0bfc..27a5dee98 100644 --- a/hub-client/src/components/render/q2-preview/blocks/CodeBlock.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/CodeBlock.tsx @@ -1,4 +1,4 @@ -import type { CodeBlock as CodeBlockType, NodeArgs } from '../../framework'; +import type { CodeBlock as CodeBlockType, NodeArgs } from '@quarto/preview-renderer/framework'; export const CodeBlock = ({ node }: NodeArgs) => { const [[id, classes, kvs], code] = node.c; diff --git a/hub-client/src/components/render/q2-preview/blocks/DefinitionList.tsx b/hub-client/src/components/render/q2-preview/blocks/DefinitionList.tsx index 10327b042..ff83328a2 100644 --- a/hub-client/src/components/render/q2-preview/blocks/DefinitionList.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/DefinitionList.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { Node } from '../../framework'; +import { Node } from '@quarto/preview-renderer/framework'; import type { BlockNode, DefinitionListBlock, InlineNode, NodeArgs, -} from '../../framework'; +} from '@quarto/preview-renderer/framework'; /** * DefinitionList → `
    term
    def
    ...
    `. Pandoc diff --git a/hub-client/src/components/render/q2-preview/blocks/Div.tsx b/hub-client/src/components/render/q2-preview/blocks/Div.tsx index 025b6bad4..88afdbe9b 100644 --- a/hub-client/src/components/render/q2-preview/blocks/Div.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/Div.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { DivBlock, NodeArgs } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { DivBlock, NodeArgs } from '@quarto/preview-renderer/framework'; export const Div = (args: NodeArgs) => { const [[id, classes, kvs]] = args.node.c; diff --git a/hub-client/src/components/render/q2-preview/blocks/Figure.tsx b/hub-client/src/components/render/q2-preview/blocks/Figure.tsx index 5fa9676f7..8d247a186 100644 --- a/hub-client/src/components/render/q2-preview/blocks/Figure.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/Figure.tsx @@ -1,5 +1,5 @@ -import { Node } from '../../framework'; -import type { BlockNode, FigureBlock, InlineNode, NodeArgs } from '../../framework'; +import { Node } from '@quarto/preview-renderer/framework'; +import type { BlockNode, FigureBlock, InlineNode, NodeArgs } from '@quarto/preview-renderer/framework'; /** * Figure →
    + optional
    . Reads `c[1][1]` diff --git a/hub-client/src/components/render/q2-preview/blocks/Header.tsx b/hub-client/src/components/render/q2-preview/blocks/Header.tsx index e555f9989..a811ea25a 100644 --- a/hub-client/src/components/render/q2-preview/blocks/Header.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/Header.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { HeaderBlock, NodeArgs } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { HeaderBlock, NodeArgs } from '@quarto/preview-renderer/framework'; const headerTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const; diff --git a/hub-client/src/components/render/q2-preview/blocks/LineBlock.tsx b/hub-client/src/components/render/q2-preview/blocks/LineBlock.tsx index 150f8d58d..2478a7b09 100644 --- a/hub-client/src/components/render/q2-preview/blocks/LineBlock.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/LineBlock.tsx @@ -1,5 +1,5 @@ -import { Node } from '../../framework'; -import type { BlockNode, InlineNode, LineBlockBlock, NodeArgs } from '../../framework'; +import { Node } from '@quarto/preview-renderer/framework'; +import type { BlockNode, InlineNode, LineBlockBlock, NodeArgs } from '@quarto/preview-renderer/framework'; /** * LineBlock → `
    ` with each line as a `
    ` diff --git a/hub-client/src/components/render/q2-preview/blocks/OrderedList.tsx b/hub-client/src/components/render/q2-preview/blocks/OrderedList.tsx index a6d8c3657..ac93f08f7 100644 --- a/hub-client/src/components/render/q2-preview/blocks/OrderedList.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/OrderedList.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, OrderedListBlock } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, OrderedListBlock } from '@quarto/preview-renderer/framework'; /** * OrderedList →
      with `start`, `type`, and `data-list-style-delim` diff --git a/hub-client/src/components/render/q2-preview/blocks/Para.tsx b/hub-client/src/components/render/q2-preview/blocks/Para.tsx index 7fcfd59a8..0e0716ba8 100644 --- a/hub-client/src/components/render/q2-preview/blocks/Para.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/Para.tsx @@ -1,4 +1,4 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, ParaBlock } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, ParaBlock } from '@quarto/preview-renderer/framework'; export const Para = (args: NodeArgs) =>

      {renderChildren(args)}

      ; diff --git a/hub-client/src/components/render/q2-preview/blocks/Plain.tsx b/hub-client/src/components/render/q2-preview/blocks/Plain.tsx index 8751609ad..9e86e7e31 100644 --- a/hub-client/src/components/render/q2-preview/blocks/Plain.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/Plain.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, PlainBlock } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, PlainBlock } from '@quarto/preview-renderer/framework'; /** Plain renders no wrapper element — its inlines flow into the * surrounding context (table cells, list items, etc.). */ diff --git a/hub-client/src/components/render/q2-preview/blocks/RawBlock.tsx b/hub-client/src/components/render/q2-preview/blocks/RawBlock.tsx index f361e96a1..b196f6783 100644 --- a/hub-client/src/components/render/q2-preview/blocks/RawBlock.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/RawBlock.tsx @@ -1,4 +1,4 @@ -import type { NodeArgs, RawBlock as RawBlockType } from '../../framework'; +import type { NodeArgs, RawBlock as RawBlockType } from '@quarto/preview-renderer/framework'; /** * RawBlock semantics: diff --git a/hub-client/src/components/render/q2-preview/blocks/Table.tsx b/hub-client/src/components/render/q2-preview/blocks/Table.tsx index 16e73020c..3b3dfbf89 100644 --- a/hub-client/src/components/render/q2-preview/blocks/Table.tsx +++ b/hub-client/src/components/render/q2-preview/blocks/Table.tsx @@ -1,5 +1,5 @@ -import { Node } from '../../framework'; -import type { BlockNode, InlineNode, NodeArgs, TableBlock } from '../../framework'; +import { Node } from '@quarto/preview-renderer/framework'; +import type { BlockNode, InlineNode, NodeArgs, TableBlock } from '@quarto/preview-renderer/framework'; /** * Table → `` with optional `` / `` / diff --git a/hub-client/src/components/render/q2-preview/custom-components.integration.test.tsx b/hub-client/src/components/render/q2-preview/custom-components.integration.test.tsx index 6049a9e1a..5be457526 100644 --- a/hub-client/src/components/render/q2-preview/custom-components.integration.test.tsx +++ b/hub-client/src/components/render/q2-preview/custom-components.integration.test.tsx @@ -19,8 +19,8 @@ import { describe, expect, it, vi } from 'vitest'; import { render } from '@testing-library/react'; -import { Ast } from '../framework'; -import type { FormatRegistry, NodeArgs, PandocAST } from '../framework'; +import { Ast } from '@quarto/preview-renderer/framework'; +import type { FormatRegistry, NodeArgs, PandocAST } from '@quarto/preview-renderer/framework'; import { previewRegistry } from './registry'; import { CALLOUT, diff --git a/hub-client/src/components/render/q2-preview/custom/Callout.tsx b/hub-client/src/components/render/q2-preview/custom/Callout.tsx index e0275746f..afff9efc7 100644 --- a/hub-client/src/components/render/q2-preview/custom/Callout.tsx +++ b/hub-client/src/components/render/q2-preview/custom/Callout.tsx @@ -2,7 +2,7 @@ import type { CustomBlockNode, NodeArgs, Slot, -} from '../../framework'; +} from '@quarto/preview-renderer/framework'; import { CALLOUT, CALLOUT_APPEARANCE_PREFIX, diff --git a/hub-client/src/components/render/q2-preview/custom/CrossrefResolvedRef.tsx b/hub-client/src/components/render/q2-preview/custom/CrossrefResolvedRef.tsx index 82a0dd76e..5c94db984 100644 --- a/hub-client/src/components/render/q2-preview/custom/CrossrefResolvedRef.tsx +++ b/hub-client/src/components/render/q2-preview/custom/CrossrefResolvedRef.tsx @@ -3,8 +3,8 @@ import type { CustomInlineNode, InlineNode, NodeArgs, -} from '../../framework'; -import { Node } from '../../framework'; +} from '@quarto/preview-renderer/framework'; +import { Node } from '@quarto/preview-renderer/framework'; import { QUARTO_XREF } from '../quartoClasses'; /** diff --git a/hub-client/src/components/render/q2-preview/custom/Equation.tsx b/hub-client/src/components/render/q2-preview/custom/Equation.tsx index 065f8b14d..1bdeae18b 100644 --- a/hub-client/src/components/render/q2-preview/custom/Equation.tsx +++ b/hub-client/src/components/render/q2-preview/custom/Equation.tsx @@ -4,8 +4,8 @@ import type { InlineNode, MathInline, NodeArgs, -} from '../../framework'; -import { Node } from '../../framework'; +} from '@quarto/preview-renderer/framework'; +import { Node } from '@quarto/preview-renderer/framework'; import { makeSlotSetter } from '../utils'; /** diff --git a/hub-client/src/components/render/q2-preview/custom/Fallback.tsx b/hub-client/src/components/render/q2-preview/custom/Fallback.tsx index 96953d415..b665dc66d 100644 --- a/hub-client/src/components/render/q2-preview/custom/Fallback.tsx +++ b/hub-client/src/components/render/q2-preview/custom/Fallback.tsx @@ -1,9 +1,9 @@ -import { renderChildren } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; import type { CustomBlockNode, CustomInlineNode, NodeArgs, -} from '../../framework'; +} from '@quarto/preview-renderer/framework'; /** * Generic CustomNode renderer for unknown / not-yet-implemented diff --git a/hub-client/src/components/render/q2-preview/custom/FloatRefTarget.tsx b/hub-client/src/components/render/q2-preview/custom/FloatRefTarget.tsx index 93890c724..37831a63c 100644 --- a/hub-client/src/components/render/q2-preview/custom/FloatRefTarget.tsx +++ b/hub-client/src/components/render/q2-preview/custom/FloatRefTarget.tsx @@ -4,8 +4,8 @@ import type { InlineNode, NodeArgs, ParaBlock, -} from '../../framework'; -import { Node } from '../../framework'; +} from '@quarto/preview-renderer/framework'; +import { Node } from '@quarto/preview-renderer/framework'; import { makeSlotSetter } from '../utils'; /** diff --git a/hub-client/src/components/render/q2-preview/custom/PreviewTitleBlock.integration.test.tsx b/hub-client/src/components/render/q2-preview/custom/PreviewTitleBlock.integration.test.tsx index cfdeb3aef..f0b566791 100644 --- a/hub-client/src/components/render/q2-preview/custom/PreviewTitleBlock.integration.test.tsx +++ b/hub-client/src/components/render/q2-preview/custom/PreviewTitleBlock.integration.test.tsx @@ -13,8 +13,8 @@ import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'; import { render } from '@testing-library/react'; -import { Ast } from '../../framework'; -import type { FormatRegistry, PandocAST } from '../../framework'; +import { Ast } from '@quarto/preview-renderer/framework'; +import type { FormatRegistry, PandocAST } from '@quarto/preview-renderer/framework'; import { previewRegistry } from '../registry'; import { PreviewTitleBlock } from './PreviewTitleBlock'; diff --git a/hub-client/src/components/render/q2-preview/custom/PreviewTitleBlock.tsx b/hub-client/src/components/render/q2-preview/custom/PreviewTitleBlock.tsx index c6e5a3c58..733cb05e1 100644 --- a/hub-client/src/components/render/q2-preview/custom/PreviewTitleBlock.tsx +++ b/hub-client/src/components/render/q2-preview/custom/PreviewTitleBlock.tsx @@ -1,5 +1,5 @@ -import type { AstProps } from '../../framework'; -import { extractMetaString, extractMetaStringList } from '../../framework'; +import type { AstProps } from '@quarto/preview-renderer/framework'; +import { extractMetaString, extractMetaStringList } from '@quarto/preview-renderer/framework'; /** * Built-in `__title_block__` synthetic-registry entry (Plan 2D Phase 7). diff --git a/hub-client/src/components/render/q2-preview/custom/Proof.tsx b/hub-client/src/components/render/q2-preview/custom/Proof.tsx index e48e51b4f..9fe24f445 100644 --- a/hub-client/src/components/render/q2-preview/custom/Proof.tsx +++ b/hub-client/src/components/render/q2-preview/custom/Proof.tsx @@ -4,8 +4,8 @@ import type { InlineNode, NodeArgs, ParaBlock, -} from '../../framework'; -import { Node } from '../../framework'; +} from '@quarto/preview-renderer/framework'; +import { Node } from '@quarto/preview-renderer/framework'; import { PROOF } from '../quartoClasses'; import { makeSlotSetter } from '../utils'; diff --git a/hub-client/src/components/render/q2-preview/custom/Theorem.tsx b/hub-client/src/components/render/q2-preview/custom/Theorem.tsx index 376a2bbe5..1c5ecf69b 100644 --- a/hub-client/src/components/render/q2-preview/custom/Theorem.tsx +++ b/hub-client/src/components/render/q2-preview/custom/Theorem.tsx @@ -4,8 +4,8 @@ import type { InlineNode, NodeArgs, ParaBlock, -} from '../../framework'; -import { Node } from '../../framework'; +} from '@quarto/preview-renderer/framework'; +import { Node } from '@quarto/preview-renderer/framework'; import { THEOREM, THEOREM_TITLE } from '../quartoClasses'; import { theoremEnvFor } from '../theoremEnvs'; import { makeSlotSetter } from '../utils'; diff --git a/hub-client/src/components/render/q2-preview/dispatchers.tsx b/hub-client/src/components/render/q2-preview/dispatchers.tsx index e1ac979bf..81d8d6933 100644 --- a/hub-client/src/components/render/q2-preview/dispatchers.tsx +++ b/hub-client/src/components/render/q2-preview/dispatchers.tsx @@ -1,13 +1,13 @@ import { useContext } from 'react'; -import { RegistryContext } from '../framework/RegistryContext'; -import { renderChildren } from '../framework'; +import { RegistryContext } from '@quarto/preview-renderer/framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; import type { BlockNode, CustomBlockNode, CustomInlineNode, InlineNode, NodeArgs, -} from '../framework/types'; +} from '@quarto/preview-renderer/framework'; const placeholderStyle: React.CSSProperties = { color: '#888', diff --git a/hub-client/src/components/render/q2-preview/entry.tsx b/hub-client/src/components/render/q2-preview/entry.tsx index 21eff5a35..ec4e012ba 100644 --- a/hub-client/src/components/render/q2-preview/entry.tsx +++ b/hub-client/src/components/render/q2-preview/entry.tsx @@ -41,8 +41,8 @@ import { extractMetaStringList, inlinesToPlainText, blocksToPlainText, -} from '../framework'; -import type { FormatRegistry, NoteInline, PandocAST } from '../framework'; +} from '@quarto/preview-renderer/framework'; +import type { FormatRegistry, NoteInline, PandocAST } from '@quarto/preview-renderer/framework'; import { Block, Inline, previewRegistry, PreviewContext } from '.'; import { AssetManifestContext } from './AssetManifestContext'; import { NoteNumberingContext } from './NoteNumberingContext'; diff --git a/hub-client/src/components/render/q2-preview/inlines/Cite.tsx b/hub-client/src/components/render/q2-preview/inlines/Cite.tsx index 42b9649ee..e40a87cb1 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Cite.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Cite.tsx @@ -1,5 +1,5 @@ -import { Node } from '../../framework'; -import type { BlockNode, CiteInline, InlineNode, NodeArgs } from '../../framework'; +import { Node } from '@quarto/preview-renderer/framework'; +import type { BlockNode, CiteInline, InlineNode, NodeArgs } from '@quarto/preview-renderer/framework'; /** * Cite renders `c[1]` (the visible inlines Pandoc fills in for the diff --git a/hub-client/src/components/render/q2-preview/inlines/Code.tsx b/hub-client/src/components/render/q2-preview/inlines/Code.tsx index e1521bc6a..b7b0f968b 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Code.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Code.tsx @@ -1,4 +1,4 @@ -import type { CodeInline, NodeArgs } from '../../framework'; +import type { CodeInline, NodeArgs } from '@quarto/preview-renderer/framework'; export const Code = ({ node }: NodeArgs) => { const [[id, classes, kvs], content] = node.c; diff --git a/hub-client/src/components/render/q2-preview/inlines/Emph.tsx b/hub-client/src/components/render/q2-preview/inlines/Emph.tsx index a1a9d9ba8..c5fc0ceda 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Emph.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Emph.tsx @@ -1,4 +1,4 @@ -import { renderChildren } from '../../framework'; -import type { EmphInline, NodeArgs } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { EmphInline, NodeArgs } from '@quarto/preview-renderer/framework'; export const Emph = (args: NodeArgs) => {renderChildren(args)}; diff --git a/hub-client/src/components/render/q2-preview/inlines/Image.tsx b/hub-client/src/components/render/q2-preview/inlines/Image.tsx index 7b5672268..f02ae107c 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Image.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Image.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; -import type { ImageInline, NodeArgs } from '../../framework'; -import { inlinesToPlainText } from '../../framework'; +import type { ImageInline, NodeArgs } from '@quarto/preview-renderer/framework'; +import { inlinesToPlainText } from '@quarto/preview-renderer/framework'; import { AssetManifestContext } from '../AssetManifestContext'; import { lookupAssetUrl } from '../utils'; diff --git a/hub-client/src/components/render/q2-preview/inlines/Link.tsx b/hub-client/src/components/render/q2-preview/inlines/Link.tsx index 6627f2c4f..738747562 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Link.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Link.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { LinkInline, NodeArgs } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { LinkInline, NodeArgs } from '@quarto/preview-renderer/framework'; export const Link = (args: NodeArgs) => { const [[id, classes, kvs], , [url, title]] = args.node.c; diff --git a/hub-client/src/components/render/q2-preview/inlines/Math.tsx b/hub-client/src/components/render/q2-preview/inlines/Math.tsx index 940bfe537..b14b99a3d 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Math.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Math.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; import katex from 'katex'; -import type { MathInline, NodeArgs } from '../../framework'; +import type { MathInline, NodeArgs } from '@quarto/preview-renderer/framework'; /** * Math (DisplayMath / InlineMath) → KaTeX-rendered HTML wrapped in a diff --git a/hub-client/src/components/render/q2-preview/inlines/Note.tsx b/hub-client/src/components/render/q2-preview/inlines/Note.tsx index c6b4fa44a..d5d0d9479 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Note.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Note.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; -import type { NodeArgs, NoteInline } from '../../framework'; -import { blocksToPlainText } from '../../framework'; +import type { NodeArgs, NoteInline } from '@quarto/preview-renderer/framework'; +import { blocksToPlainText } from '@quarto/preview-renderer/framework'; import { NoteNumberingContext } from '../NoteNumberingContext'; import { FOOTNOTE_REF } from '../quartoClasses'; diff --git a/hub-client/src/components/render/q2-preview/inlines/Quoted.tsx b/hub-client/src/components/render/q2-preview/inlines/Quoted.tsx index 818df7f41..da33404fb 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Quoted.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Quoted.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, QuotedInline } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, QuotedInline } from '@quarto/preview-renderer/framework'; const QUOTE_CHARS = { SingleQuote: ['‘', '’'] as const, // ‘ ’ diff --git a/hub-client/src/components/render/q2-preview/inlines/RawInline.tsx b/hub-client/src/components/render/q2-preview/inlines/RawInline.tsx index bb6527877..40c1a0d8a 100644 --- a/hub-client/src/components/render/q2-preview/inlines/RawInline.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/RawInline.tsx @@ -1,4 +1,4 @@ -import type { NodeArgs, RawInlineInline } from '../../framework'; +import type { NodeArgs, RawInlineInline } from '@quarto/preview-renderer/framework'; /** * RawInline semantics mirror RawBlock: diff --git a/hub-client/src/components/render/q2-preview/inlines/SmallCaps.tsx b/hub-client/src/components/render/q2-preview/inlines/SmallCaps.tsx index ceca35ed9..2ba21fa89 100644 --- a/hub-client/src/components/render/q2-preview/inlines/SmallCaps.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/SmallCaps.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, SmallCapsInline } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, SmallCapsInline } from '@quarto/preview-renderer/framework'; export const SmallCaps = (args: NodeArgs) => ( {renderChildren(args)} diff --git a/hub-client/src/components/render/q2-preview/inlines/Span.tsx b/hub-client/src/components/render/q2-preview/inlines/Span.tsx index ad0454424..62f105ade 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Span.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Span.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, SpanInline } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, SpanInline } from '@quarto/preview-renderer/framework'; export const Span = (args: NodeArgs) => { const [[id, classes, kvs]] = args.node.c; diff --git a/hub-client/src/components/render/q2-preview/inlines/Str.tsx b/hub-client/src/components/render/q2-preview/inlines/Str.tsx index 1a82694c3..a1f524df8 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Str.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Str.tsx @@ -1,3 +1,3 @@ -import type { NodeArgs, StrInline } from '../../framework'; +import type { NodeArgs, StrInline } from '@quarto/preview-renderer/framework'; export const Str = ({ node }: NodeArgs) => <>{node.c}; diff --git a/hub-client/src/components/render/q2-preview/inlines/Strikeout.tsx b/hub-client/src/components/render/q2-preview/inlines/Strikeout.tsx index d64cf6595..5477a1fc5 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Strikeout.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Strikeout.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, StrikeoutInline } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, StrikeoutInline } from '@quarto/preview-renderer/framework'; export const Strikeout = (args: NodeArgs) => ( {renderChildren(args)} diff --git a/hub-client/src/components/render/q2-preview/inlines/Strong.tsx b/hub-client/src/components/render/q2-preview/inlines/Strong.tsx index b6eb62279..2156e65ef 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Strong.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Strong.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, StrongInline } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, StrongInline } from '@quarto/preview-renderer/framework'; export const Strong = (args: NodeArgs) => ( {renderChildren(args)} diff --git a/hub-client/src/components/render/q2-preview/inlines/Subscript.tsx b/hub-client/src/components/render/q2-preview/inlines/Subscript.tsx index 2884ac9d8..e7271e7af 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Subscript.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Subscript.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, SubscriptInline } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, SubscriptInline } from '@quarto/preview-renderer/framework'; export const Subscript = (args: NodeArgs) => ( {renderChildren(args)} diff --git a/hub-client/src/components/render/q2-preview/inlines/Superscript.tsx b/hub-client/src/components/render/q2-preview/inlines/Superscript.tsx index 329facda9..1c18f9525 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Superscript.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Superscript.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, SuperscriptInline } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, SuperscriptInline } from '@quarto/preview-renderer/framework'; export const Superscript = (args: NodeArgs) => ( {renderChildren(args)} diff --git a/hub-client/src/components/render/q2-preview/inlines/Underline.tsx b/hub-client/src/components/render/q2-preview/inlines/Underline.tsx index 1001353af..05427de25 100644 --- a/hub-client/src/components/render/q2-preview/inlines/Underline.tsx +++ b/hub-client/src/components/render/q2-preview/inlines/Underline.tsx @@ -1,5 +1,5 @@ -import { renderChildren } from '../../framework'; -import type { NodeArgs, UnderlineInline } from '../../framework'; +import { renderChildren } from '@quarto/preview-renderer/framework'; +import type { NodeArgs, UnderlineInline } from '@quarto/preview-renderer/framework'; export const Underline = (args: NodeArgs) => ( {renderChildren(args)} diff --git a/hub-client/src/components/render/q2-preview/q2-preview.integration.test.tsx b/hub-client/src/components/render/q2-preview/q2-preview.integration.test.tsx index df271bc70..f557cac02 100644 --- a/hub-client/src/components/render/q2-preview/q2-preview.integration.test.tsx +++ b/hub-client/src/components/render/q2-preview/q2-preview.integration.test.tsx @@ -29,7 +29,7 @@ import { describe, it, expect, vi } from 'vitest'; import { render, fireEvent } from '@testing-library/react'; -import { Ast } from '../framework'; +import { Ast } from '@quarto/preview-renderer/framework'; import type { BlockNode, InlineNode, @@ -37,7 +37,7 @@ import type { ParaBlock, PandocAST, FormatRegistry, -} from '../framework'; +} from '@quarto/preview-renderer/framework'; import { previewRegistry } from './registry'; import { AssetManifestContext } from './AssetManifestContext'; import { diff --git a/hub-client/src/components/render/q2-preview/registry.ts b/hub-client/src/components/render/q2-preview/registry.ts index e99ad0767..ecc3f50d4 100644 --- a/hub-client/src/components/render/q2-preview/registry.ts +++ b/hub-client/src/components/render/q2-preview/registry.ts @@ -1,4 +1,4 @@ -import type { FormatRegistry } from '../framework'; +import type { FormatRegistry } from '@quarto/preview-renderer/framework'; import * as Blocks from './blocks'; import * as Inlines from './inlines'; import * as Custom from './custom'; diff --git a/hub-client/src/components/render/q2-preview/utils.tsx b/hub-client/src/components/render/q2-preview/utils.tsx index 8ce4cb102..7f98e79ac 100644 --- a/hub-client/src/components/render/q2-preview/utils.tsx +++ b/hub-client/src/components/render/q2-preview/utils.tsx @@ -29,9 +29,9 @@ import type { CustomInlineNode, InlineNode, Slot, -} from '../framework'; -import type { Attr } from '../framework/types'; -import { Node } from '../framework'; +} from '@quarto/preview-renderer/framework'; +import type { Attr } from '@quarto/preview-renderer/framework'; +import { Node } from '@quarto/preview-renderer/framework'; // --- asset URL lookup ------------------------------------------------ diff --git a/hub-client/src/hooks/useCursorToSlide.ts b/hub-client/src/hooks/useCursorToSlide.ts index a181b42b9..2d8352d6a 100644 --- a/hub-client/src/hooks/useCursorToSlide.ts +++ b/hub-client/src/hooks/useCursorToSlide.ts @@ -7,7 +7,7 @@ import { useMemo } from 'react'; import type { Symbol } from '@quarto/preview-renderer/types/intelligence'; import { parseSlides } from '../components/render/ReactAstSlideRenderer'; -import type { PandocAST } from '../components/render/framework/types'; +import type { PandocAST } from '@quarto/preview-renderer/framework'; interface SlideMapping { /** The starting line (0-based) where this slide begins. */ diff --git a/hub-client/src/hooks/useSlideThumbnails.tsx b/hub-client/src/hooks/useSlideThumbnails.tsx index aa2a7b722..25d52905f 100644 --- a/hub-client/src/hooks/useSlideThumbnails.tsx +++ b/hub-client/src/hooks/useSlideThumbnails.tsx @@ -10,7 +10,7 @@ import ReactDOM from 'react-dom/client'; import html2canvas from 'html2canvas'; import type { Symbol } from '@quarto/preview-renderer/types/intelligence'; import { parseSlides, renderSlide } from '../components/render/ReactAstSlideRenderer'; -import type { PandocAST } from '../components/render/framework/types'; +import type { PandocAST } from '@quarto/preview-renderer/framework'; /** * Map from symbol line number to thumbnail data URL. diff --git a/ts-packages/preview-renderer/package.json b/ts-packages/preview-renderer/package.json index 85d1fba35..26ad2471e 100644 --- a/ts-packages/preview-renderer/package.json +++ b/ts-packages/preview-renderer/package.json @@ -16,6 +16,11 @@ "source": "./src/index.ts", "import": "./dist/index.js" }, + "./framework": { + "types": "./src/framework/index.ts", + "source": "./src/framework/index.ts", + "import": "./dist/framework/index.js" + }, "./types/*": { "types": "./src/types/*.ts", "source": "./src/types/*.ts", diff --git a/hub-client/src/components/render/framework/Ast.tsx b/ts-packages/preview-renderer/src/framework/Ast.tsx similarity index 97% rename from hub-client/src/components/render/framework/Ast.tsx rename to ts-packages/preview-renderer/src/framework/Ast.tsx index 3d2243f7b..d20b8e561 100644 --- a/hub-client/src/components/render/framework/Ast.tsx +++ b/ts-packages/preview-renderer/src/framework/Ast.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { RegistryContext } from './RegistryContext'; import { unwrapCustomNodes } from './customNode'; import type { PandocAST } from './types'; -import type { SourceInfoPool } from '@quarto/preview-renderer/types/sourceInfo'; +import type { SourceInfoPool } from '../types/sourceInfo'; interface AstPropsCommon { /** Current file path for resolving relative image paths */ diff --git a/hub-client/src/components/render/framework/RegistryContext.tsx b/ts-packages/preview-renderer/src/framework/RegistryContext.tsx similarity index 92% rename from hub-client/src/components/render/framework/RegistryContext.tsx rename to ts-packages/preview-renderer/src/framework/RegistryContext.tsx index a33464cca..6c9bb7d6d 100644 --- a/hub-client/src/components/render/framework/RegistryContext.tsx +++ b/ts-packages/preview-renderer/src/framework/RegistryContext.tsx @@ -1,5 +1,5 @@ import { createContext } from 'react'; -import type { SourceInfoPool } from '@quarto/preview-renderer/types/sourceInfo'; +import type { SourceInfoPool } from '../types/sourceInfo'; /** * Context that carries the active format's registry to the dispatchers, diff --git a/hub-client/src/components/render/framework/customNode.test.ts b/ts-packages/preview-renderer/src/framework/customNode.test.ts similarity index 100% rename from hub-client/src/components/render/framework/customNode.test.ts rename to ts-packages/preview-renderer/src/framework/customNode.test.ts diff --git a/hub-client/src/components/render/framework/customNode.ts b/ts-packages/preview-renderer/src/framework/customNode.ts similarity index 100% rename from hub-client/src/components/render/framework/customNode.ts rename to ts-packages/preview-renderer/src/framework/customNode.ts diff --git a/hub-client/src/components/render/framework/dispatch.tsx b/ts-packages/preview-renderer/src/framework/dispatch.tsx similarity index 99% rename from hub-client/src/components/render/framework/dispatch.tsx rename to ts-packages/preview-renderer/src/framework/dispatch.tsx index a1e7e30a9..e4640bdc5 100644 --- a/hub-client/src/components/render/framework/dispatch.tsx +++ b/ts-packages/preview-renderer/src/framework/dispatch.tsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import { RegistryContext } from './RegistryContext'; -import { isAtomicSourceInfo, ATOMIC_SYNTHETIC_KINDS } from '@quarto/preview-renderer/utils/sourceInfo'; -import { isAtomicCustomNode } from '@quarto/preview-renderer/utils/atomicCustomNodes'; +import { isAtomicSourceInfo, ATOMIC_SYNTHETIC_KINDS } from '../utils/sourceInfo'; +import { isAtomicCustomNode } from '../utils/atomicCustomNodes'; import type { BlockNode, InlineNode, diff --git a/hub-client/src/components/render/framework/index.ts b/ts-packages/preview-renderer/src/framework/index.ts similarity index 100% rename from hub-client/src/components/render/framework/index.ts rename to ts-packages/preview-renderer/src/framework/index.ts diff --git a/hub-client/src/components/render/framework/meta.test.ts b/ts-packages/preview-renderer/src/framework/meta.test.ts similarity index 100% rename from hub-client/src/components/render/framework/meta.test.ts rename to ts-packages/preview-renderer/src/framework/meta.test.ts diff --git a/hub-client/src/components/render/framework/meta.ts b/ts-packages/preview-renderer/src/framework/meta.ts similarity index 100% rename from hub-client/src/components/render/framework/meta.ts rename to ts-packages/preview-renderer/src/framework/meta.ts diff --git a/hub-client/src/components/render/framework/plainText.test.ts b/ts-packages/preview-renderer/src/framework/plainText.test.ts similarity index 100% rename from hub-client/src/components/render/framework/plainText.test.ts rename to ts-packages/preview-renderer/src/framework/plainText.test.ts diff --git a/hub-client/src/components/render/framework/plainText.ts b/ts-packages/preview-renderer/src/framework/plainText.ts similarity index 100% rename from hub-client/src/components/render/framework/plainText.ts rename to ts-packages/preview-renderer/src/framework/plainText.ts diff --git a/hub-client/src/components/render/framework/types.ts b/ts-packages/preview-renderer/src/framework/types.ts similarity index 100% rename from hub-client/src/components/render/framework/types.ts rename to ts-packages/preview-renderer/src/framework/types.ts From b16372068a525c2fcaa5fc5ddb05324a5d1ebd4f Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 10:22:46 -0500 Subject: [PATCH 007/108] docs(hub-client): changelog entry for bd-hfjj Phase 3 Co-Authored-By: Claude Opus 4.7 (1M context) --- hub-client/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/hub-client/changelog.md b/hub-client/changelog.md index 434e5fa72..4f2bf0507 100644 --- a/hub-client/changelog.md +++ b/hub-client/changelog.md @@ -15,6 +15,7 @@ be in reverse chronological order (latest first). ### 2026-05-13 +- [`7e45d2bf`](https://github.com/quarto-dev/q2/commits/7e45d2bf): Internal — move the rendering framework (Ast, dispatch, RegistryContext, plainText, meta, customNode) from `hub-client/src/components/render/framework/` into `@quarto/preview-renderer` (bd-hfjj Phase 3). Hub-client now imports the framework barrel through `@quarto/preview-renderer/framework`. No user-visible change. - [`5d8bd2b3`](https://github.com/quarto-dev/q2/commits/5d8bd2b3): Internal — start carving the preview pane into shared workspace packages (bd-hfjj Phase 2). Five type modules and seven utilities move from `hub-client/src/` into the new `@quarto/preview-renderer` package; hub-client now imports them via sub-paths. No user-visible change; this prepares for the `q2 preview` SPA reusing the exact same rendering code. ### 2026-05-10 From 9c508c9138f59f8a08052913aedd4d3a044d62fe Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 10:47:47 -0500 Subject: [PATCH 008/108] refactor(q2-preview): move services to preview-runtime (bd-hfjj Phase 5, swapped before Phase 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the WASM-bridge / automerge-sync / user-grammar services out of hub-client into the new @quarto/preview-runtime workspace, plus pulls iframePostProcessor (deferred from Phase 2) into @quarto/preview-renderer now that its `vfsReadFile` / `vfsReadBinaryFile` dependency lives in runtime. To preview-runtime: src/wasmRenderer.ts (+ test) src/automergeSync.ts (+ test) src/userGrammar/Discovery.ts (renamed; + test) src/userGrammar/Cache.ts (renamed; + test) src/userGrammar/Highlight.ts (renamed; + WASM-init test) src/test-utils/mockSyncClient.ts (test helper) src/test-utils/mockWasm.ts (test helper) src/wasm-quarto-hub-client.d.ts (mirrored from hub-client so the package typechecks standalone) src/vite-shims.d.ts (`*.wasm?url` and `/src/wasm-js-bridge/*.js` ambient declarations) To preview-renderer: src/utils/iframePostProcessor.ts (+ unit and integration tests) Phase reordering: the original plan had Phase 4 (q2-preview / iframe wrappers / overlays) before Phase 5 (services). Swapping is *safer*: most Phase-4 files import `services/wasmRenderer` or `utils/iframePostProcessor`, so moving services first means the remaining Phase-4 movers already point at @quarto/preview-runtime when their turn comes. Hub-client's full test surface keeps validating the renderer-side code throughout this swap, so any service regression surfaces immediately. See "Phase ordering note" in the plan. Re-decided 2026-05-13: `assetWalker.ts` does NOT move to preview-runtime. It stays inside `q2-preview/` and moves with it in Phase 4. Rationale: assetWalker is an AST walker that *uses* VFS, not a VFS service; moving it to runtime would create a circular preview-runtime → preview-renderer dependency via `vfsPaths`. The plan's prior signal ("the test moves alongside since it tests the runtime function") wasn't load-bearing. Cross-package wiring changes: - preview-renderer gains @quarto/preview-runtime as a workspace dep (iframePostProcessor imports `vfsReadFile`). - preview-runtime gains @quarto/preview-renderer + @quarto/pandoc-types + @quarto/quarto-automerge-schema as workspace deps. The preview-renderer dep is for the wire-format `Diagnostic` / `RenderResponse` re-exports from `types/diagnostic`. - preview-runtime/package.json adds sub-path exports `./userGrammar/*` and `./test-utils/*`, plus a `test:wasm` script (foundation for moving the wasm-test runner later if useful). WASM build-time path resolution: `wasmRenderer.ts`'s lazy `import('../wasm-js-bridge/sass.js')` was rewritten to `'/src/wasm-js-bridge/sass.js'` — the same Vite-root convention the Rust WASM module already uses via `wasm-bindgen raw_module = "/src/wasm-js-bridge/sass.js"`. Every consumer (hub-client today, the future q2-preview SPA) must host the bridge files at `src/wasm-js-bridge/` and Vite resolves uniformly. An ambient `declare module '/src/wasm-js-bridge/*.js'` shim in `preview-runtime/src/vite-shims.d.ts` keeps tsc happy. Hub-client import rewrites: Two passes via `/tmp/rewrite-phase5-imports.py`: - Pass 1: `/services/wasmRenderer`, `/services/automergeSync`, `/utils/iframePostProcessor`, and the renamed userGrammar paths. - Pass 2: short-form intra-services imports (`./wasmRenderer`, `./automergeSync`) that the first pass's pattern didn't cover. Manually fixed three categories the regex couldn't: - `vi.mock('./wasmRenderer', ...)` and `vi.mock('./automergeSync', ...)` in test files (rewritten to mock '@quarto/preview-runtime'). - Inline `import('../services/automergeSync').ActorIdentity` in Editor.tsx. - `await import('./framework')` in parity.integration.test.tsx (Phase 3 leftover that the framework rewrite caught here). Hub-client's three vitest configs gain `@quarto/preview-runtime` aliases to source (same pattern as `@quarto/preview-renderer`). `Highlight.wasm.test.ts`'s `repoRoot` computation updated from `../../..` to `../../../..` to account for the deeper directory. All green: - preview-runtime: 60 tests / 6 files, tsc build clean - preview-renderer: 133 tests / 10 files (unit) + 8 integration, tsc build clean - hub-client: 525 unit tests, 173 integration tests, 79 wasm tests, `build:all` clean - cargo xtask verify --skip-rust-tests: all 9 steps pass Snapshot tests: no snapshot files changed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-11-hub-client-decomposition.md | 95 ++++++++--- hub-client/src/App.tsx | 4 +- hub-client/src/components/Editor.tsx | 6 +- hub-client/src/components/ProjectSelector.tsx | 2 +- .../src/components/ReplayDrawer.test.tsx | 4 +- hub-client/src/components/ReplayDrawer.tsx | 4 +- .../render/DoubleBufferedIframe.tsx | 2 +- .../src/components/render/MorphIframe.tsx | 2 +- hub-client/src/components/render/Preview.tsx | 4 +- .../components/render/PreviewErrorOverlay.tsx | 2 +- .../src/components/render/PreviewRouter.tsx | 2 +- .../render/ReactAstSlideRenderer.tsx | 2 +- .../src/components/render/ReactPreview.tsx | 2 +- .../Q2PreviewIframe.integration.test.tsx | 2 +- .../render/q2-preview/Q2PreviewIframe.tsx | 2 +- .../render/q2-preview/assetWalker.test.ts | 2 +- .../render/q2-preview/assetWalker.ts | 2 +- hub-client/src/components/tabs/AboutTab.tsx | 2 +- hub-client/src/hooks/useAutomergeSync.test.ts | 6 +- hub-client/src/hooks/useAutomergeSync.ts | 2 +- .../src/hooks/useIframePostProcessor.ts | 4 +- hub-client/src/hooks/usePresence.test.ts | 2 +- hub-client/src/hooks/usePresence.ts | 2 +- hub-client/src/hooks/useReplayMode.test.ts | 4 +- hub-client/src/hooks/useReplayMode.ts | 2 +- .../assetManifestProject.wasm.test.ts | 2 +- .../customNodeWireFormatProject.wasm.test.ts | 2 +- hub-client/src/services/debugApi.test.ts | 8 +- hub-client/src/services/debugApi.ts | 4 +- .../src/services/intelligenceService.ts | 2 +- .../src/services/presenceService.test.ts | 9 +- hub-client/src/services/presenceService.ts | 2 +- hub-client/src/services/smokeAll.wasm.test.ts | 4 +- .../src/services/templateService.test.ts | 6 +- hub-client/src/services/templateService.ts | 2 +- .../services/userGrammarParity.wasm.test.ts | 2 +- hub-client/src/test-hooks.ts | 2 +- hub-client/vitest.config.ts | 8 +- hub-client/vitest.integration.config.ts | 1 + hub-client/vitest.wasm.config.ts | 3 + package-lock.json | 4 + ts-packages/preview-renderer/package.json | 1 + .../iframePostProcessor.integration.test.ts | 0 .../src/utils/iframePostProcessor.test.ts | 0 .../src/utils/iframePostProcessor.ts | 4 +- ts-packages/preview-runtime/package.json | 13 ++ .../src}/automergeSync.test.ts | 2 +- .../preview-runtime/src}/automergeSync.ts | 0 ts-packages/preview-runtime/src/index.ts | 17 +- .../src/test-utils/mockSyncClient.ts | 0 .../src/test-utils/mockWasm.ts | 0 .../src/userGrammar/Cache.test.ts | 6 +- .../preview-runtime/src/userGrammar/Cache.ts | 4 +- .../src/userGrammar/Discovery.test.ts | 2 +- .../src/userGrammar/Discovery.ts | 0 .../src/userGrammar/Highlight.ts | 0 .../src/userGrammar/Highlight.wasm.test.ts | 5 +- .../preview-runtime/src/vite-shims.d.ts | 27 ++++ .../src/wasm-quarto-hub-client.d.ts | 151 ++++++++++++++++++ .../preview-runtime/src}/wasmRenderer.test.ts | 0 .../preview-runtime/src}/wasmRenderer.ts | 19 ++- ts-packages/preview-runtime/vitest.config.ts | 16 ++ .../vitest.integration.config.ts | 12 ++ 63 files changed, 407 insertions(+), 96 deletions(-) rename {hub-client => ts-packages/preview-renderer}/src/utils/iframePostProcessor.integration.test.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/iframePostProcessor.test.ts (100%) rename {hub-client => ts-packages/preview-renderer}/src/utils/iframePostProcessor.ts (98%) rename {hub-client/src/services => ts-packages/preview-runtime/src}/automergeSync.test.ts (99%) rename {hub-client/src/services => ts-packages/preview-runtime/src}/automergeSync.ts (100%) rename {hub-client => ts-packages/preview-runtime}/src/test-utils/mockSyncClient.ts (100%) rename {hub-client => ts-packages/preview-runtime}/src/test-utils/mockWasm.ts (100%) rename hub-client/src/services/userGrammarCache.test.ts => ts-packages/preview-runtime/src/userGrammar/Cache.test.ts (98%) rename hub-client/src/services/userGrammarCache.ts => ts-packages/preview-runtime/src/userGrammar/Cache.ts (98%) rename hub-client/src/services/userGrammarDiscovery.test.ts => ts-packages/preview-runtime/src/userGrammar/Discovery.test.ts (98%) rename hub-client/src/services/userGrammarDiscovery.ts => ts-packages/preview-runtime/src/userGrammar/Discovery.ts (100%) rename hub-client/src/services/userGrammarHighlight.ts => ts-packages/preview-runtime/src/userGrammar/Highlight.ts (100%) rename hub-client/src/services/userGrammarHighlight.wasm.test.ts => ts-packages/preview-runtime/src/userGrammar/Highlight.wasm.test.ts (96%) create mode 100644 ts-packages/preview-runtime/src/vite-shims.d.ts create mode 100644 ts-packages/preview-runtime/src/wasm-quarto-hub-client.d.ts rename {hub-client/src/services => ts-packages/preview-runtime/src}/wasmRenderer.test.ts (100%) rename {hub-client/src/services => ts-packages/preview-runtime/src}/wasmRenderer.ts (97%) diff --git a/claude-notes/plans/2026-05-11-hub-client-decomposition.md b/claude-notes/plans/2026-05-11-hub-client-decomposition.md index fdb8875d6..5864c16de 100644 --- a/claude-notes/plans/2026-05-11-hub-client-decomposition.md +++ b/claude-notes/plans/2026-05-11-hub-client-decomposition.md @@ -3,7 +3,7 @@ date: 2026-05-11 updated: 2026-05-13 branch: beads/bd-hfjj-hub-client-decomposition-shared beads: bd-hfjj (sub-epic of bd-kw93) -status: approved 2026-05-13; Phases 0–3 complete; Phase 4 next +status: approved 2026-05-13; Phases 0–3 + 5 complete; Phase 4 next (Phase 5 ↔ Phase 4 order swapped on 2026-05-13 — see §Phase ordering note) --- # Hub-client decomposition: shared preview-pane packages for hub-client + q2-preview-spa @@ -257,10 +257,6 @@ Note: `types/project.test.ts` moves with `types/project.ts`. (+ any colocated tests) - `hub-client/src/services/automergeSync.ts` → `automergeSync.ts` (+ tests) -- `hub-client/src/components/render/q2-preview/assetWalker.ts` → - `assetWalker.ts` (the *implementation*; the test moves alongside - it here as well, since it tests the runtime function. Update the - preview-renderer cross-ref note above.) - `hub-client/src/services/userGrammarDiscovery.ts` → `userGrammar/Discovery.ts` - `hub-client/src/services/userGrammarCache.ts` → @@ -268,6 +264,18 @@ Note: `types/project.test.ts` moves with `types/project.ts`. - `hub-client/src/services/userGrammarHighlight.ts` → `userGrammar/Highlight.ts` +**Note (2026-05-13):** the original plan also moved +`q2-preview/assetWalker.ts` here. Re-deciding: it stays *with* +`q2-preview/` (i.e., it moves to preview-renderer in Phase 4 along +with the rest of `q2-preview/`). Rationale: assetWalker is an +AST-walker that *uses* VFS, not a VFS service itself, and moving it +into runtime would force a circular preview-runtime → preview-renderer +dependency (assetWalker imports `vfsPaths` from preview-renderer; the +plan's stated invariant is unidirectional renderer → runtime). The +plan's prior reasoning ("the test moves alongside since it tests the +runtime function") was a weak signal — the test exercises the manifest +walk, not the runtime per se. + ### Staying in hub-client (explicit list, for review) To make the boundary review-able, here are the preview-adjacent @@ -478,6 +486,22 @@ The TDD policy applies as "tests-stay-green-across-move." We are not adding behavior; we are relocating it. Each phase's invariant is: the same set of tests passes before and after the move. +### Phase ordering note (2026-05-13) + +The phases below are labeled in their *original* order. As of +2026-05-13, execution order is **0, 1, 2, 3, 5, 4, 6, 7** — Phase 5 +(services to preview-runtime) is done *before* Phase 4 (q2-preview / +iframe wrappers / overlays). + +Reason: most Phase-4 files import `services/wasmRenderer` or +`utils/iframePostProcessor` (which itself was deferred from Phase 2 to +Phase 5). Moving services first means Phase 4's imports already point +at `@quarto/preview-runtime` by the time we move them. The original +"renderer first" ordering was a hedge against WASM-test breakage; +"services first" turns out to be *safer* because hub-client's full +test surface keeps validating the renderer-side code during the +services move, so any regression surfaces immediately. + ### Phase 0 — Pre-flight (no code changes) - [x] Verify the starting workspace builds clean. @@ -667,25 +691,33 @@ internally consistent. ### Phase 5 — Move services to preview-runtime -- [ ] Move `wasmRenderer.ts` → `preview-runtime/src/wasmRenderer.ts`. -- [ ] Move `automergeSync.ts` → `preview-runtime/src/automergeSync.ts`. -- [ ] Move `assetWalker.ts` (from `q2-preview/`) → - `preview-runtime/src/assetWalker.ts`. -- [ ] Move `iframePostProcessor.ts` (+ `.test.ts`, +*(Executed before Phase 4 — see §Phase ordering note.)* + +- [x] Move `wasmRenderer.ts` (+ test) → `preview-runtime/src/`. +- [x] Move `automergeSync.ts` (+ test) → `preview-runtime/src/`. +- [x] ~~Move `assetWalker.ts` (from `q2-preview/`) → + `preview-runtime/src/assetWalker.ts`.~~ + *Re-decided 2026-05-13 (see §"Moving to preview-runtime" + note): assetWalker stays with `q2-preview/` and moves in + Phase 4. Rationale there.* +- [x] Move `iframePostProcessor.ts` (+ `.test.ts`, `.integration.test.ts`) from hub-client to `preview-renderer/src/utils/` — deferred from Phase 2 because it imports `vfsReadFile`/`vfsReadBinaryFile` from `wasmRenderer`. After this phase the import resolves via `@quarto/preview-runtime`. -- [ ] Move the three `userGrammar*` files → +- [x] Move the three `userGrammar*` files → `preview-runtime/src/userGrammar/` (renamed: `Discovery.ts`, `Cache.ts`, `Highlight.ts`). -- [ ] Move colocated tests. -- [ ] `preview-renderer`'s `assetWalker.ts` consumers (only the - Q2PreviewIframe boot path) now import from - `@quarto/preview-runtime`. This is a renderer→runtime - dependency — declare it in `preview-renderer/package.json`'s - `dependencies` (workspace `*`). +- [x] Move colocated tests (Discovery.test, Cache.test, + Highlight.wasm.test). Updated `Highlight.wasm.test.ts`'s + `repoRoot` computation from `../../..` (relative to + `hub-client/src/services/`) to `../../../..` (relative to + `ts-packages/preview-runtime/src/userGrammar/`). +- [x] `iframePostProcessor`'s consumers (now in preview-renderer) + import from `@quarto/preview-runtime`. This is a + renderer→runtime dependency — declared in + `preview-renderer/package.json`'s `dependencies` (workspace `*`). Tradeoff to note: this means preview-renderer is no longer "pure React with no WASM transitive." It pulls in @@ -698,16 +730,35 @@ internally consistent. components that drive a render, runtime = the things they call out to." That's still useful as a split. -- [ ] Update every hub-client import of these services to +- [x] Update every hub-client import of these services to `@quarto/preview-runtime`. -- [ ] Configure `preview-runtime/vitest.config.ts` (and - `vitest.wasm.config.ts` if needed) with the WASM alias. - Confirm WASM-using tests run. + *(38 imports across 31 files via `/tmp/rewrite-phase5-imports.py`, + including short-form intra-`services/` imports — the script ran in + two passes. Caught the unusual cases manually: `vi.mock('./...')` + and inline `import('...').T` type imports.)* +- [x] Configure `preview-runtime/vitest.config.ts` (and + `vitest.integration.config.ts`) with the WASM alias plus + workspace-package aliases (mirrors hub-client's pattern). +- [x] Set up the type plumbing so both tsc (per-package build) and + hub-client's transitive compilation see ambient module + declarations: `vite-shims.d.ts` for the `*.wasm?url` and + `/src/wasm-js-bridge/*.js` paths, plus `wasm-quarto-hub-client.d.ts` + copied alongside it. Pulled in by triple-slash references at the + top of `preview-runtime/src/index.ts`. +- [x] Update hub-client's three vitest configs to alias + `@quarto/preview-runtime` to `ts-packages/preview-runtime/src` + (Vite resolves through the `source` condition; vitest needs the + explicit alias on fresh clones). **Acceptance:** - Same as Phase 4, plus: - `npm test --workspace @quarto/preview-runtime` passes, - including any WASM-using tests. + including any WASM-using tests. ✓ (60 tests / 6 files) +- hub-client `test`, `test:integration`, `test:wasm`, `build:all` + all green. ✓ +- `cargo xtask verify --skip-rust-tests` green. ✓ +- preview-renderer tests still pass (133 unit + 8 integration). ✓ +- Both packages build via `tsc`. ✓ - hub-client still builds and tests cleanly. - Manual: `npm run dev` in hub-client → preview pane renders → WASM init happens → q2-preview format displays. diff --git a/hub-client/src/App.tsx b/hub-client/src/App.tsx index ad1cdca61..9312bba55 100644 --- a/hub-client/src/App.tsx +++ b/hub-client/src/App.tsx @@ -27,8 +27,8 @@ import { createNewProject, type ActorIdentity, type EditorContentChange, -} from './services/automergeSync'; -import type { ProjectFile } from './services/wasmRenderer'; +} from '@quarto/preview-runtime'; +import type { ProjectFile } from '@quarto/preview-runtime'; import * as projectStorage from './services/projectStorage'; import { installDebugApi } from './services/debugApi'; import { getUserIdentity, updateUserName } from './services/userSettings'; diff --git a/hub-client/src/components/Editor.tsx b/hub-client/src/components/Editor.tsx index 58b3028f0..908b0c336 100644 --- a/hub-client/src/components/Editor.tsx +++ b/hub-client/src/components/Editor.tsx @@ -12,8 +12,8 @@ import { renameFile, exportProjectAsZip, type EditorContentChange, -} from '../services/automergeSync'; -import { vfsAddFile, isWasmReady } from '../services/wasmRenderer'; +} from '@quarto/preview-runtime'; +import { vfsAddFile, isWasmReady } from '@quarto/preview-runtime'; import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; import { registerIntelligenceProviders, disposeIntelligenceProviders } from '../services/monacoProviders'; import { processFileForUpload } from '../services/resourceService'; @@ -55,7 +55,7 @@ interface Props { /** Callback to update URL when file changes */ onNavigateToFile: (filePath: string, options?: { anchor?: string; replace?: boolean }) => void; /** Actor ID -> identity mapping from the IndexDocument */ - identities?: Record; + identities?: Record; /** Whether the project is connected to the sync server */ isOnline: boolean; } diff --git a/hub-client/src/components/ProjectSelector.tsx b/hub-client/src/components/ProjectSelector.tsx index 980070eeb..128db02e1 100644 --- a/hub-client/src/components/ProjectSelector.tsx +++ b/hub-client/src/components/ProjectSelector.tsx @@ -11,7 +11,7 @@ import { createProject as wasmCreateProject, type ProjectChoice, type ProjectFile, -} from '../services/wasmRenderer'; +} from '@quarto/preview-runtime'; import { DEFAULT_SYNC_SERVER, buildProjectSetLinkUrl } from '../utils/routing'; import ShareDialog from './ShareDialog'; import './ProjectSelector.css'; diff --git a/hub-client/src/components/ReplayDrawer.test.tsx b/hub-client/src/components/ReplayDrawer.test.tsx index 337db5b38..bdbe6f51a 100644 --- a/hub-client/src/components/ReplayDrawer.test.tsx +++ b/hub-client/src/components/ReplayDrawer.test.tsx @@ -9,9 +9,9 @@ import { render, screen, fireEvent, cleanup } from '@testing-library/react'; import ReplayDrawer from './ReplayDrawer'; import type { ReplayState, ReplayControls } from '../hooks/useReplayMode'; -// Mock getActorId from automergeSync +// Mock getActorId from automergeSync (now in @quarto/preview-runtime) let mockActorId: string | null = null; -vi.mock('../services/automergeSync', () => ({ +vi.mock('@quarto/preview-runtime', () => ({ getActorId: () => mockActorId, })); diff --git a/hub-client/src/components/ReplayDrawer.tsx b/hub-client/src/components/ReplayDrawer.tsx index 5b23b75b4..f73b40972 100644 --- a/hub-client/src/components/ReplayDrawer.tsx +++ b/hub-client/src/components/ReplayDrawer.tsx @@ -1,8 +1,8 @@ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import type { ReplayState, ReplayControls } from '../hooks/useReplayMode'; import { actorColor } from '../hooks/useReplayMode'; -import type { ActorIdentity } from '../services/automergeSync'; -import { getActorId } from '../services/automergeSync'; +import type { ActorIdentity } from '@quarto/preview-runtime'; +import { getActorId } from '@quarto/preview-runtime'; import './ReplayDrawer.css'; interface Props { diff --git a/hub-client/src/components/render/DoubleBufferedIframe.tsx b/hub-client/src/components/render/DoubleBufferedIframe.tsx index 092811877..dd4a05079 100644 --- a/hub-client/src/components/render/DoubleBufferedIframe.tsx +++ b/hub-client/src/components/render/DoubleBufferedIframe.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect, useCallback, useImperativeHandle } from 'react'; import type { Ref } from 'react'; -import { postProcessIframe } from '../../utils/iframePostProcessor'; +import { postProcessIframe } from '@quarto/preview-renderer/utils/iframePostProcessor'; // Methods exposed via ref export interface DoubleBufferedIframeHandle { diff --git a/hub-client/src/components/render/MorphIframe.tsx b/hub-client/src/components/render/MorphIframe.tsx index 2861e22ff..e3cc30293 100644 --- a/hub-client/src/components/render/MorphIframe.tsx +++ b/hub-client/src/components/render/MorphIframe.tsx @@ -1,7 +1,7 @@ import { useRef, useEffect, useCallback, useImperativeHandle } from 'react'; import type { Ref } from 'react'; import morphdom from 'morphdom'; -import { postProcessIframe } from '../../utils/iframePostProcessor'; +import { postProcessIframe } from '@quarto/preview-renderer/utils/iframePostProcessor'; // Methods exposed via ref export interface MorphIframeHandle { diff --git a/hub-client/src/components/render/Preview.tsx b/hub-client/src/components/render/Preview.tsx index f1375d2a2..0f561f610 100644 --- a/hub-client/src/components/render/Preview.tsx +++ b/hub-client/src/components/render/Preview.tsx @@ -2,8 +2,8 @@ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import type * as Monaco from 'monaco-editor'; import type { FileEntry } from '@quarto/preview-renderer/types/project'; import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; -import { renderToHtml, isWasmReady, setScrollSyncEnabled, type Pass1Failure } from '../../services/wasmRenderer'; -import { getFileContent, getBinaryFileContent } from '../../services/automergeSync'; +import { renderToHtml, isWasmReady, setScrollSyncEnabled, type Pass1Failure } from '@quarto/preview-runtime'; +import { getFileContent, getBinaryFileContent } from '@quarto/preview-runtime'; import { useScrollSync } from '../../hooks/useScrollSync'; import { useSelectionSync } from '../../hooks/useSelectionSync'; import { PreviewErrorOverlay } from './PreviewErrorOverlay'; diff --git a/hub-client/src/components/render/PreviewErrorOverlay.tsx b/hub-client/src/components/render/PreviewErrorOverlay.tsx index 68c92cfaa..07f7cf571 100644 --- a/hub-client/src/components/render/PreviewErrorOverlay.tsx +++ b/hub-client/src/components/render/PreviewErrorOverlay.tsx @@ -1,5 +1,5 @@ import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; -import type { Pass1Failure } from '../../services/wasmRenderer'; +import type { Pass1Failure } from '@quarto/preview-runtime'; import { stripAnsi } from '@quarto/preview-renderer/utils/stripAnsi'; import { usePreference } from '../../hooks/usePreference'; diff --git a/hub-client/src/components/render/PreviewRouter.tsx b/hub-client/src/components/render/PreviewRouter.tsx index 3b1b5f292..bc508eb3d 100644 --- a/hub-client/src/components/render/PreviewRouter.tsx +++ b/hub-client/src/components/render/PreviewRouter.tsx @@ -3,7 +3,7 @@ import type * as Monaco from 'monaco-editor'; import type { FileEntry } from '@quarto/preview-renderer/types/project'; import { isQmdFile } from '@quarto/preview-renderer/types/project'; import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; -import { parseQmdToAst, isWasmReady, initWasm } from '../../services/wasmRenderer'; +import { parseQmdToAst, isWasmReady, initWasm } from '@quarto/preview-runtime'; import Preview from './Preview'; import ReactPreview from './ReactPreview'; import { FallbackView, NonQmdPlaceholderView } from './PreviewStaticInfoViews'; diff --git a/hub-client/src/components/render/ReactAstSlideRenderer.tsx b/hub-client/src/components/render/ReactAstSlideRenderer.tsx index c919fb8ee..59cb29fd2 100644 --- a/hub-client/src/components/render/ReactAstSlideRenderer.tsx +++ b/hub-client/src/components/render/ReactAstSlideRenderer.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { AspectRatioScaler } from '../render/AspectRatioScaler'; import katex from 'katex'; import 'katex/dist/katex.min.css'; -import { vfsReadFile, vfsReadBinaryFile } from '../../services/wasmRenderer'; +import { vfsReadFile, vfsReadBinaryFile } from '@quarto/preview-runtime'; import { resolveRelativePath, guessMimeType } from '@quarto/preview-renderer/utils/vfsPaths'; import type { PandocAST, diff --git a/hub-client/src/components/render/ReactPreview.tsx b/hub-client/src/components/render/ReactPreview.tsx index 6e31e58cc..d5dddb7cd 100644 --- a/hub-client/src/components/render/ReactPreview.tsx +++ b/hub-client/src/components/render/ReactPreview.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import type * as Monaco from 'monaco-editor'; import type { FileEntry } from '@quarto/preview-renderer/types/project'; import type { Diagnostic } from '@quarto/preview-renderer/types/diagnostic'; -import { parseQmdToAst, renderPageInProject, isWasmReady, incrementalWriteQmd } from '../../services/wasmRenderer'; +import { parseQmdToAst, renderPageInProject, isWasmReady, incrementalWriteQmd } from '@quarto/preview-runtime'; import { pipelineKindForFormat } from '../../utils/pipelineKind'; import { stripAnsi } from '@quarto/preview-renderer/utils/stripAnsi'; import { PreviewErrorOverlay } from './PreviewErrorOverlay'; diff --git a/hub-client/src/components/render/q2-preview/Q2PreviewIframe.integration.test.tsx b/hub-client/src/components/render/q2-preview/Q2PreviewIframe.integration.test.tsx index a4b5094ae..a1e02dcca 100644 --- a/hub-client/src/components/render/q2-preview/Q2PreviewIframe.integration.test.tsx +++ b/hub-client/src/components/render/q2-preview/Q2PreviewIframe.integration.test.tsx @@ -18,7 +18,7 @@ import { act } from 'react'; // map so the asset-manifest test can vary which paths return which bytes. let mockBytes: string | null = null; const mockBinaryByPath: Map = new Map(); -vi.mock('../../../services/wasmRenderer', () => ({ +vi.mock('@quarto/preview-runtime', () => ({ vfsReadFile: vi.fn(() => { if (mockBytes === null) return { success: false }; return { success: true, content: mockBytes }; diff --git a/hub-client/src/components/render/q2-preview/Q2PreviewIframe.tsx b/hub-client/src/components/render/q2-preview/Q2PreviewIframe.tsx index ced2c10f7..3f6564156 100644 --- a/hub-client/src/components/render/q2-preview/Q2PreviewIframe.tsx +++ b/hub-client/src/components/render/q2-preview/Q2PreviewIframe.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { vfsReadFile } from '../../../services/wasmRenderer'; +import { vfsReadFile } from '@quarto/preview-runtime'; import { DEFAULT_CSS_ARTIFACT_PATH } from '@quarto/preview-renderer/types/artifactPaths'; import { buildAssetManifest, type ManifestCacheEntry } from './assetWalker'; diff --git a/hub-client/src/components/render/q2-preview/assetWalker.test.ts b/hub-client/src/components/render/q2-preview/assetWalker.test.ts index 211a091b6..3a9d36f3d 100644 --- a/hub-client/src/components/render/q2-preview/assetWalker.test.ts +++ b/hub-client/src/components/render/q2-preview/assetWalker.test.ts @@ -12,7 +12,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; const vfsMock = vi.fn(); -vi.mock('../../../services/wasmRenderer', () => ({ +vi.mock('@quarto/preview-runtime', () => ({ vfsReadBinaryFile: (path: string) => vfsMock(path), })); diff --git a/hub-client/src/components/render/q2-preview/assetWalker.ts b/hub-client/src/components/render/q2-preview/assetWalker.ts index 3cbf16b9a..5d18dd211 100644 --- a/hub-client/src/components/render/q2-preview/assetWalker.ts +++ b/hub-client/src/components/render/q2-preview/assetWalker.ts @@ -18,7 +18,7 @@ * stable image content keeps the same blob URL across renders. */ -import { vfsReadBinaryFile } from '../../../services/wasmRenderer'; +import { vfsReadBinaryFile } from '@quarto/preview-runtime'; import { resolveRelativePath, guessMimeType } from '@quarto/preview-renderer/utils/vfsPaths'; export interface ManifestCacheEntry { diff --git a/hub-client/src/components/tabs/AboutTab.tsx b/hub-client/src/components/tabs/AboutTab.tsx index e25d70c79..c8706d3cc 100644 --- a/hub-client/src/components/tabs/AboutTab.tsx +++ b/hub-client/src/components/tabs/AboutTab.tsx @@ -8,7 +8,7 @@ */ import { useState, useEffect } from 'react'; -import { renderContentToHtml, isWasmReady } from '../../services/wasmRenderer'; +import { renderContentToHtml, isWasmReady } from '@quarto/preview-runtime'; import changelogMd from '../../../changelog.md?raw'; import moreInfoMd from '../../../resources/more-info.md?raw'; import './AboutTab.css'; diff --git a/hub-client/src/hooks/useAutomergeSync.test.ts b/hub-client/src/hooks/useAutomergeSync.test.ts index 26a319ae6..4cec41ad4 100644 --- a/hub-client/src/hooks/useAutomergeSync.test.ts +++ b/hub-client/src/hooks/useAutomergeSync.test.ts @@ -7,8 +7,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, act } from '@testing-library/react'; -// Mock automergeSync service -vi.mock('../services/automergeSync', () => ({ +// Mock automergeSync service (now @quarto/preview-runtime) +vi.mock('@quarto/preview-runtime', () => ({ getFileContent: vi.fn(), setImmediateFileChangeCallback: vi.fn(), })); @@ -19,7 +19,7 @@ vi.mock('../utils/diffToMonacoEdits', () => ({ })); import { useAutomergeSync } from './useAutomergeSync'; -import { getFileContent, setImmediateFileChangeCallback } from '../services/automergeSync'; +import { getFileContent, setImmediateFileChangeCallback } from '@quarto/preview-runtime'; import { diffToMonacoEdits } from '../utils/diffToMonacoEdits'; import type { FileEntry } from '@quarto/preview-renderer/types/project'; import { setVisibility, resetVisibility, fireWindowFocus } from '../test-utils/visibility'; diff --git a/hub-client/src/hooks/useAutomergeSync.ts b/hub-client/src/hooks/useAutomergeSync.ts index 4003ccd9f..48773e14f 100644 --- a/hub-client/src/hooks/useAutomergeSync.ts +++ b/hub-client/src/hooks/useAutomergeSync.ts @@ -29,7 +29,7 @@ import { getFileContent, setImmediateFileChangeCallback, type EditorContentChange, -} from '../services/automergeSync'; +} from '@quarto/preview-runtime'; import { diffToMonacoEdits } from '../utils/diffToMonacoEdits'; interface UseAutomergeSyncOptions { diff --git a/hub-client/src/hooks/useIframePostProcessor.ts b/hub-client/src/hooks/useIframePostProcessor.ts index 7a01f9c2f..83f0a43da 100644 --- a/hub-client/src/hooks/useIframePostProcessor.ts +++ b/hub-client/src/hooks/useIframePostProcessor.ts @@ -9,8 +9,8 @@ import { useCallback, useEffect, useState, useMemo } from 'react'; import type { RefObject } from 'react'; -import { postProcessIframe } from '../utils/iframePostProcessor'; -import type { PostProcessOptions } from '../utils/iframePostProcessor'; +import { postProcessIframe } from '@quarto/preview-renderer/utils/iframePostProcessor'; +import type { PostProcessOptions } from '@quarto/preview-renderer/utils/iframePostProcessor'; /** * Hook for post-processing iframe content after render. diff --git a/hub-client/src/hooks/usePresence.test.ts b/hub-client/src/hooks/usePresence.test.ts index 807a9aa94..8a7c328cb 100644 --- a/hub-client/src/hooks/usePresence.test.ts +++ b/hub-client/src/hooks/usePresence.test.ts @@ -49,7 +49,7 @@ vi.mock('../services/presenceService', () => ({ let localDoc: Doc<{ text: string }> = A.from({ text: '' }); let handleThrows = false; -vi.mock('../services/automergeSync', () => ({ +vi.mock('@quarto/preview-runtime', () => ({ getFileHandle: vi.fn(() => ({ doc: () => { if (handleThrows) throw new Error('handle unavailable'); diff --git a/hub-client/src/hooks/usePresence.ts b/hub-client/src/hooks/usePresence.ts index d84f7c604..ce72d5344 100644 --- a/hub-client/src/hooks/usePresence.ts +++ b/hub-client/src/hooks/usePresence.ts @@ -25,7 +25,7 @@ import { getLocalPeerId, type PresenceState, } from '../services/presenceService'; -import { getFileHandle } from '../services/automergeSync'; +import { getFileHandle } from '@quarto/preview-runtime'; /** * Options for the usePresence hook. diff --git a/hub-client/src/hooks/useReplayMode.test.ts b/hub-client/src/hooks/useReplayMode.test.ts index 42c8c19a6..9cefbd5b8 100644 --- a/hub-client/src/hooks/useReplayMode.test.ts +++ b/hub-client/src/hooks/useReplayMode.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, act } from '@testing-library/react'; -vi.mock('../services/automergeSync', () => ({ +vi.mock('@quarto/preview-runtime', () => ({ getFileHandle: vi.fn(), updateFileContent: vi.fn(), })); @@ -20,7 +20,7 @@ import { useReplayMode } from './useReplayMode'; import { getFileHandle, updateFileContent, -} from '../services/automergeSync'; +} from '@quarto/preview-runtime'; import { createReplaySession } from '@quarto/quarto-sync-client'; const mockGetFileHandle = vi.mocked(getFileHandle); diff --git a/hub-client/src/hooks/useReplayMode.ts b/hub-client/src/hooks/useReplayMode.ts index ae2fea331..5fd5ca3ba 100644 --- a/hub-client/src/hooks/useReplayMode.ts +++ b/hub-client/src/hooks/useReplayMode.ts @@ -2,7 +2,7 @@ import { useState, useCallback, useRef } from 'react'; import { getFileHandle, updateFileContent, -} from '../services/automergeSync'; +} from '@quarto/preview-runtime'; import { createReplaySession, type ReplaySession, diff --git a/hub-client/src/services/assetManifestProject.wasm.test.ts b/hub-client/src/services/assetManifestProject.wasm.test.ts index 28e8e4a14..dcbd4a0c1 100644 --- a/hub-client/src/services/assetManifestProject.wasm.test.ts +++ b/hub-client/src/services/assetManifestProject.wasm.test.ts @@ -17,7 +17,7 @@ import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; import { readFile } from 'fs/promises'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; -import { initWasm, vfsAddFile, vfsAddBinaryFile, vfsClear } from './wasmRenderer'; +import { initWasm, vfsAddFile, vfsAddBinaryFile, vfsClear } from '@quarto/preview-runtime'; import { buildAssetManifest, type ManifestCacheEntry } from '../components/render/q2-preview/assetWalker'; interface RenderResponse { diff --git a/hub-client/src/services/customNodeWireFormatProject.wasm.test.ts b/hub-client/src/services/customNodeWireFormatProject.wasm.test.ts index 81caa133b..e63eca287 100644 --- a/hub-client/src/services/customNodeWireFormatProject.wasm.test.ts +++ b/hub-client/src/services/customNodeWireFormatProject.wasm.test.ts @@ -19,7 +19,7 @@ import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; import { readFile } from 'fs/promises'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; -import { initWasm, vfsAddFile, vfsClear } from './wasmRenderer'; +import { initWasm, vfsAddFile, vfsClear } from '@quarto/preview-runtime'; interface RenderResponse { success: boolean; diff --git a/hub-client/src/services/debugApi.test.ts b/hub-client/src/services/debugApi.test.ts index 031e5bec5..6a0b20147 100644 --- a/hub-client/src/services/debugApi.test.ts +++ b/hub-client/src/services/debugApi.test.ts @@ -33,8 +33,12 @@ const wasmRendererMocks = vi.hoisted(() => ({ vfsReadBinaryFile: vi.fn<(path: string) => { success: boolean; content?: string }>(), })); -vi.mock('./automergeSync', () => automergeSyncMocks); -vi.mock('./wasmRenderer', () => wasmRendererMocks); +// Both modules now live in @quarto/preview-runtime; the barrel re-exports +// everything, so a single mock of the barrel covers both surfaces. +vi.mock('@quarto/preview-runtime', () => ({ + ...automergeSyncMocks, + ...wasmRendererMocks, +})); import { installDebugApi, diff --git a/hub-client/src/services/debugApi.ts b/hub-client/src/services/debugApi.ts index eb18360fb..3fbe019c0 100644 --- a/hub-client/src/services/debugApi.ts +++ b/hub-client/src/services/debugApi.ts @@ -26,7 +26,7 @@ import { createFile, createBinaryFile, deleteFile, -} from './automergeSync'; +} from '@quarto/preview-runtime'; import { renderToHtml, setRenderListener, @@ -34,7 +34,7 @@ import { vfsReadBinaryFile, type RenderResult, type RenderToHtmlOptions, -} from './wasmRenderer'; +} from '@quarto/preview-runtime'; export interface QuartoDebugProjectInfo { id: string; diff --git a/hub-client/src/services/intelligenceService.ts b/hub-client/src/services/intelligenceService.ts index b367a8d9b..2ca266b22 100644 --- a/hub-client/src/services/intelligenceService.ts +++ b/hub-client/src/services/intelligenceService.ts @@ -19,7 +19,7 @@ import type { LspFoldingRangesResponse, LspDiagnosticsResponse, } from '@quarto/preview-renderer/types/intelligence'; -import { initWasm } from './wasmRenderer'; +import { initWasm } from '@quarto/preview-runtime'; import { isQmdFile } from '@quarto/preview-renderer/types/project'; // Re-export types for convenience diff --git a/hub-client/src/services/presenceService.test.ts b/hub-client/src/services/presenceService.test.ts index 3a7fa2126..5f73d1223 100644 --- a/hub-client/src/services/presenceService.test.ts +++ b/hub-client/src/services/presenceService.test.ts @@ -39,13 +39,14 @@ vi.mock('./userSettings', () => ({ }), })); -// Mock the automergeSync module. Tests that need a live handle install one -// into state.currentHandle directly via `_getStateForTesting`. -vi.mock('./automergeSync', () => ({ +// Mock the automergeSync module (now @quarto/preview-runtime). Tests +// that need a live handle install one into state.currentHandle directly +// via `_getStateForTesting`. +vi.mock('@quarto/preview-runtime', () => ({ getFileHandle: vi.fn().mockReturnValue(null), })); -import { getFileHandle } from './automergeSync'; +import { getFileHandle } from '@quarto/preview-runtime'; const mockGetFileHandle = vi.mocked(getFileHandle); describe('presenceService', () => { diff --git a/hub-client/src/services/presenceService.ts b/hub-client/src/services/presenceService.ts index eca15e810..44403e72e 100644 --- a/hub-client/src/services/presenceService.ts +++ b/hub-client/src/services/presenceService.ts @@ -7,7 +7,7 @@ import type { DocHandle, DocHandleEphemeralMessagePayload } from '@automerge/automerge-repo'; import { next as A } from '@automerge/automerge'; -import { getFileHandle } from './automergeSync'; +import { getFileHandle } from '@quarto/preview-runtime'; import { getUserIdentity } from './userSettings'; import type { UserSettings } from './storage/types'; diff --git a/hub-client/src/services/smokeAll.wasm.test.ts b/hub-client/src/services/smokeAll.wasm.test.ts index 5de59fbff..537f5910b 100644 --- a/hub-client/src/services/smokeAll.wasm.test.ts +++ b/hub-client/src/services/smokeAll.wasm.test.ts @@ -14,8 +14,8 @@ import { fileURLToPath } from 'url'; import { parse as parseYaml } from 'yaml'; import { JSDOM } from 'jsdom'; -import { discoverUserGrammars } from './userGrammarDiscovery'; -import { loadUserGrammar } from './userGrammarHighlight'; +import { discoverUserGrammars } from '@quarto/preview-runtime/userGrammar/Discovery'; +import { loadUserGrammar } from '@quarto/preview-runtime/userGrammar/Highlight'; // --------------------------------------------------------------------------- // Types diff --git a/hub-client/src/services/templateService.test.ts b/hub-client/src/services/templateService.test.ts index 828f72108..6b75f40ca 100644 --- a/hub-client/src/services/templateService.test.ts +++ b/hub-client/src/services/templateService.test.ts @@ -8,8 +8,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { discoverTemplates, hasTemplates } from './templateService'; -// Mock the wasmRenderer module -vi.mock('./wasmRenderer', () => ({ +// Mock the wasmRenderer module (now @quarto/preview-runtime) +vi.mock('@quarto/preview-runtime', () => ({ vfsListFiles: vi.fn(), vfsReadFile: vi.fn(), })); @@ -19,7 +19,7 @@ vi.mock('wasm-quarto-hub-client', () => ({ prepare_template: vi.fn(), })); -import { vfsListFiles, vfsReadFile } from './wasmRenderer'; +import { vfsListFiles, vfsReadFile } from '@quarto/preview-runtime'; import { prepare_template } from 'wasm-quarto-hub-client'; const mockVfsListFiles = vi.mocked(vfsListFiles); diff --git a/hub-client/src/services/templateService.ts b/hub-client/src/services/templateService.ts index f08678720..8fcb13947 100644 --- a/hub-client/src/services/templateService.ts +++ b/hub-client/src/services/templateService.ts @@ -5,7 +5,7 @@ * Templates are .qmd files with optional template-name metadata for display names. */ -import { vfsReadFile, vfsListFiles } from './wasmRenderer'; +import { vfsReadFile, vfsListFiles } from '@quarto/preview-runtime'; // Use dynamic import to avoid requiring WASM at module load time let prepareTemplateFunc: ((content: string) => string) | null = null; diff --git a/hub-client/src/services/userGrammarParity.wasm.test.ts b/hub-client/src/services/userGrammarParity.wasm.test.ts index 1506486f4..6a14fb486 100644 --- a/hub-client/src/services/userGrammarParity.wasm.test.ts +++ b/hub-client/src/services/userGrammarParity.wasm.test.ts @@ -55,7 +55,7 @@ import { readFile } from 'fs/promises'; import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; -import { loadUserGrammar, type UserGrammarHighlighter } from './userGrammarHighlight'; +import { loadUserGrammar, type UserGrammarHighlighter } from '@quarto/preview-runtime/userGrammar/Highlight'; type SpanTriple = [number, number, string]; diff --git a/hub-client/src/test-hooks.ts b/hub-client/src/test-hooks.ts index 009b9a0f1..69b094e7d 100644 --- a/hub-client/src/test-hooks.ts +++ b/hub-client/src/test-hooks.ts @@ -13,7 +13,7 @@ * out of the production bundle entirely. */ import * as projectStorage from './services/projectStorage'; -import * as wasmRenderer from './services/wasmRenderer'; +import * as wasmRenderer from '@quarto/preview-runtime'; declare global { interface Window { diff --git a/hub-client/vitest.config.ts b/hub-client/vitest.config.ts index c6c00a367..3a5b40a0a 100644 --- a/hub-client/vitest.config.ts +++ b/hub-client/vitest.config.ts @@ -11,10 +11,12 @@ export default mergeConfig( alias: { '@quarto/quarto-automerge-schema': path.resolve(__dirname, '../ts-packages/quarto-automerge-schema/src/index.ts'), '@quarto/quarto-sync-client': path.resolve(__dirname, '../ts-packages/quarto-sync-client/src/index.ts'), - // Sub-path aware: preview-renderer exposes types/* and utils/*. - // Aliasing to the src dir lets `@quarto/preview-renderer/utils/` - // resolve to `/utils/.ts` via Vite's default extension list. + // Sub-path aware: preview-renderer exposes types/* and utils/*, + // preview-runtime exposes userGrammar/* and test-utils/*. + // Aliasing to the src dir lets `@quarto///` resolve + // to `//.ts` via Vite's default extension list. '@quarto/preview-renderer': path.resolve(__dirname, '../ts-packages/preview-renderer/src'), + '@quarto/preview-runtime': path.resolve(__dirname, '../ts-packages/preview-runtime/src'), }, }, test: { diff --git a/hub-client/vitest.integration.config.ts b/hub-client/vitest.integration.config.ts index 56fee6db2..88ab799c5 100644 --- a/hub-client/vitest.integration.config.ts +++ b/hub-client/vitest.integration.config.ts @@ -12,6 +12,7 @@ export default mergeConfig( '@quarto/quarto-automerge-schema': path.resolve(__dirname, '../ts-packages/quarto-automerge-schema/src/index.ts'), '@quarto/quarto-sync-client': path.resolve(__dirname, '../ts-packages/quarto-sync-client/src/index.ts'), '@quarto/preview-renderer': path.resolve(__dirname, '../ts-packages/preview-renderer/src'), + '@quarto/preview-runtime': path.resolve(__dirname, '../ts-packages/preview-runtime/src'), }, }, test: { diff --git a/hub-client/vitest.wasm.config.ts b/hub-client/vitest.wasm.config.ts index 1f3517360..b711e5a7b 100644 --- a/hub-client/vitest.wasm.config.ts +++ b/hub-client/vitest.wasm.config.ts @@ -27,6 +27,9 @@ export default mergeConfig( // Map it to the actual source directory for tests. '/src': path.resolve(__dirname, 'src'), '@quarto/preview-renderer': path.resolve(__dirname, '../ts-packages/preview-renderer/src'), + '@quarto/preview-runtime': path.resolve(__dirname, '../ts-packages/preview-runtime/src'), + '@quarto/quarto-automerge-schema': path.resolve(__dirname, '../ts-packages/quarto-automerge-schema/src/index.ts'), + '@quarto/quarto-sync-client': path.resolve(__dirname, '../ts-packages/quarto-sync-client/src/index.ts'), }, }, }), diff --git a/package-lock.json b/package-lock.json index 842a9c034..6dfabf4e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7712,6 +7712,8 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@quarto/preview-runtime": "*", + "@quarto/quarto-automerge-schema": "*", "morphdom": "^2.7.8", "react": "^19.2.0", "react-dom": "^19.2.0" @@ -7733,6 +7735,8 @@ "dependencies": { "@automerge/automerge": "^2.2.9", "@automerge/automerge-repo": "^2.5.1", + "@quarto/pandoc-types": "*", + "@quarto/preview-renderer": "*", "@quarto/quarto-automerge-schema": "*", "@quarto/quarto-sync-client": "*", "web-tree-sitter": "^0.26.8" diff --git a/ts-packages/preview-renderer/package.json b/ts-packages/preview-renderer/package.json index 26ad2471e..12d5cfe22 100644 --- a/ts-packages/preview-renderer/package.json +++ b/ts-packages/preview-renderer/package.json @@ -44,6 +44,7 @@ "test:watch": "vitest" }, "dependencies": { + "@quarto/preview-runtime": "*", "@quarto/quarto-automerge-schema": "*", "morphdom": "^2.7.8", "react": "^19.2.0", diff --git a/hub-client/src/utils/iframePostProcessor.integration.test.ts b/ts-packages/preview-renderer/src/utils/iframePostProcessor.integration.test.ts similarity index 100% rename from hub-client/src/utils/iframePostProcessor.integration.test.ts rename to ts-packages/preview-renderer/src/utils/iframePostProcessor.integration.test.ts diff --git a/hub-client/src/utils/iframePostProcessor.test.ts b/ts-packages/preview-renderer/src/utils/iframePostProcessor.test.ts similarity index 100% rename from hub-client/src/utils/iframePostProcessor.test.ts rename to ts-packages/preview-renderer/src/utils/iframePostProcessor.test.ts diff --git a/hub-client/src/utils/iframePostProcessor.ts b/ts-packages/preview-renderer/src/utils/iframePostProcessor.ts similarity index 98% rename from hub-client/src/utils/iframePostProcessor.ts rename to ts-packages/preview-renderer/src/utils/iframePostProcessor.ts index 9c08e8767..98595b796 100644 --- a/hub-client/src/utils/iframePostProcessor.ts +++ b/ts-packages/preview-renderer/src/utils/iframePostProcessor.ts @@ -9,8 +9,8 @@ * switch the active editor file (bd-lnd3). */ -import { vfsReadFile, vfsReadBinaryFile } from '../services/wasmRenderer'; -import { resolveRelativePath, guessMimeType } from '@quarto/preview-renderer/utils/vfsPaths'; +import { vfsReadFile, vfsReadBinaryFile } from '@quarto/preview-runtime'; +import { resolveRelativePath, guessMimeType } from './vfsPaths'; /** * VFS path under which the website renderer flushes its diff --git a/ts-packages/preview-runtime/package.json b/ts-packages/preview-runtime/package.json index c30982210..6f1a59912 100644 --- a/ts-packages/preview-runtime/package.json +++ b/ts-packages/preview-runtime/package.json @@ -15,6 +15,16 @@ "types": "./src/index.ts", "source": "./src/index.ts", "import": "./dist/index.js" + }, + "./test-utils/*": { + "types": "./src/test-utils/*.ts", + "source": "./src/test-utils/*.ts", + "import": "./dist/test-utils/*.js" + }, + "./userGrammar/*": { + "types": "./src/userGrammar/*.ts", + "source": "./src/userGrammar/*.ts", + "import": "./dist/userGrammar/*.js" } }, "files": [ @@ -26,11 +36,14 @@ "clean": "rm -rf dist", "test": "vitest run", "test:integration": "vitest run --config vitest.integration.config.ts", + "test:wasm": "vitest run --config vitest.wasm.config.ts", "test:watch": "vitest" }, "dependencies": { "@automerge/automerge": "^2.2.9", "@automerge/automerge-repo": "^2.5.1", + "@quarto/pandoc-types": "*", + "@quarto/preview-renderer": "*", "@quarto/quarto-automerge-schema": "*", "@quarto/quarto-sync-client": "*", "web-tree-sitter": "^0.26.8" diff --git a/hub-client/src/services/automergeSync.test.ts b/ts-packages/preview-runtime/src/automergeSync.test.ts similarity index 99% rename from hub-client/src/services/automergeSync.test.ts rename to ts-packages/preview-runtime/src/automergeSync.test.ts index e494e043f..2fd6c701e 100644 --- a/hub-client/src/services/automergeSync.test.ts +++ b/ts-packages/preview-runtime/src/automergeSync.test.ts @@ -19,7 +19,7 @@ import { _setClientForTesting, _getCallbacksForTesting, } from './automergeSync'; -import { createMockSyncClient, type MockSyncClient } from '../test-utils/mockSyncClient'; +import { createMockSyncClient, type MockSyncClient } from './test-utils/mockSyncClient'; // Mock the wasmRenderer module to avoid WASM initialization vi.mock('./wasmRenderer', () => ({ diff --git a/hub-client/src/services/automergeSync.ts b/ts-packages/preview-runtime/src/automergeSync.ts similarity index 100% rename from hub-client/src/services/automergeSync.ts rename to ts-packages/preview-runtime/src/automergeSync.ts diff --git a/ts-packages/preview-runtime/src/index.ts b/ts-packages/preview-runtime/src/index.ts index cb0ff5c3b..c75156a18 100644 --- a/ts-packages/preview-runtime/src/index.ts +++ b/ts-packages/preview-runtime/src/index.ts @@ -1 +1,16 @@ -export {}; +/// +/// + +// Public API for @quarto/preview-runtime. +// +// Re-exports the WASM-renderer + automerge-sync + user-grammar surface +// so consumers can write `import { vfsReadFile, ... } from '@quarto/preview-runtime'`. +// Internal sub-modules are reachable via sub-path exports (see package.json) +// when callers need finer-grained imports (e.g. testing helpers). +// +// The triple-slash reference above pulls in ambient module declarations +// (sass JS bridge, `*.wasm?url` asset URLs) so consumers like hub-client, +// which transitively typechecks our `.ts` sources, see them in scope. + +export * from './wasmRenderer'; +export * from './automergeSync'; diff --git a/hub-client/src/test-utils/mockSyncClient.ts b/ts-packages/preview-runtime/src/test-utils/mockSyncClient.ts similarity index 100% rename from hub-client/src/test-utils/mockSyncClient.ts rename to ts-packages/preview-runtime/src/test-utils/mockSyncClient.ts diff --git a/hub-client/src/test-utils/mockWasm.ts b/ts-packages/preview-runtime/src/test-utils/mockWasm.ts similarity index 100% rename from hub-client/src/test-utils/mockWasm.ts rename to ts-packages/preview-runtime/src/test-utils/mockWasm.ts diff --git a/hub-client/src/services/userGrammarCache.test.ts b/ts-packages/preview-runtime/src/userGrammar/Cache.test.ts similarity index 98% rename from hub-client/src/services/userGrammarCache.test.ts rename to ts-packages/preview-runtime/src/userGrammar/Cache.test.ts index c7ac8bbd3..6dbcafeaf 100644 --- a/hub-client/src/services/userGrammarCache.test.ts +++ b/ts-packages/preview-runtime/src/userGrammar/Cache.test.ts @@ -10,12 +10,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { UserGrammarCache } from './userGrammarCache'; -import type { GrammarDescriptor } from './userGrammarDiscovery'; +import { UserGrammarCache } from './Cache'; +import type { GrammarDescriptor } from './Discovery'; import type { UserGrammarHighlighter, LoadUserGrammarArgs, -} from './userGrammarHighlight'; +} from './Highlight'; interface FakeBinary { bytes: Uint8Array; diff --git a/hub-client/src/services/userGrammarCache.ts b/ts-packages/preview-runtime/src/userGrammar/Cache.ts similarity index 98% rename from hub-client/src/services/userGrammarCache.ts rename to ts-packages/preview-runtime/src/userGrammar/Cache.ts index f49d5e5e1..9b744a286 100644 --- a/hub-client/src/services/userGrammarCache.ts +++ b/ts-packages/preview-runtime/src/userGrammar/Cache.ts @@ -18,11 +18,11 @@ * `loadUserGrammar` with an in-memory factory. */ -import type { GrammarDescriptor } from './userGrammarDiscovery'; +import type { GrammarDescriptor } from './Discovery'; import type { LoadUserGrammarArgs, UserGrammarHighlighter, -} from './userGrammarHighlight'; +} from './Highlight'; /** * Minimal surface that the cache needs from a `JsUserGrammars` diff --git a/hub-client/src/services/userGrammarDiscovery.test.ts b/ts-packages/preview-runtime/src/userGrammar/Discovery.test.ts similarity index 98% rename from hub-client/src/services/userGrammarDiscovery.test.ts rename to ts-packages/preview-runtime/src/userGrammar/Discovery.test.ts index fc84d7eda..277427637 100644 --- a/hub-client/src/services/userGrammarDiscovery.test.ts +++ b/ts-packages/preview-runtime/src/userGrammar/Discovery.test.ts @@ -14,7 +14,7 @@ import { describe, expect, it } from 'vitest'; -import { discoverUserGrammars } from './userGrammarDiscovery'; +import { discoverUserGrammars } from './Discovery'; describe('discoverUserGrammars', () => { it('returns [] for a project with no grammars directory', () => { diff --git a/hub-client/src/services/userGrammarDiscovery.ts b/ts-packages/preview-runtime/src/userGrammar/Discovery.ts similarity index 100% rename from hub-client/src/services/userGrammarDiscovery.ts rename to ts-packages/preview-runtime/src/userGrammar/Discovery.ts diff --git a/hub-client/src/services/userGrammarHighlight.ts b/ts-packages/preview-runtime/src/userGrammar/Highlight.ts similarity index 100% rename from hub-client/src/services/userGrammarHighlight.ts rename to ts-packages/preview-runtime/src/userGrammar/Highlight.ts diff --git a/hub-client/src/services/userGrammarHighlight.wasm.test.ts b/ts-packages/preview-runtime/src/userGrammar/Highlight.wasm.test.ts similarity index 96% rename from hub-client/src/services/userGrammarHighlight.wasm.test.ts rename to ts-packages/preview-runtime/src/userGrammar/Highlight.wasm.test.ts index fa1a6d2ec..40d2d180f 100644 --- a/hub-client/src/services/userGrammarHighlight.wasm.test.ts +++ b/ts-packages/preview-runtime/src/userGrammar/Highlight.wasm.test.ts @@ -15,7 +15,7 @@ import { readFile } from 'fs/promises'; import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; -import { loadUserGrammar, type UserGrammarHighlighter } from './userGrammarHighlight'; +import { loadUserGrammar, type UserGrammarHighlighter } from './Highlight'; type SpanTriple = [number, number, string]; @@ -23,7 +23,8 @@ let highlighter: UserGrammarHighlighter; beforeAll(async () => { const __dirname = dirname(fileURLToPath(import.meta.url)); - const repoRoot = resolve(__dirname, '../../..'); + // ts-packages/preview-runtime/src/userGrammar/ → repo root is 4 levels up. + const repoRoot = resolve(__dirname, '../../../..'); const fixtureDir = join( repoRoot, 'crates/quarto-highlight/tests/fixtures/user-grammar-toml', diff --git a/ts-packages/preview-runtime/src/vite-shims.d.ts b/ts-packages/preview-runtime/src/vite-shims.d.ts new file mode 100644 index 000000000..036165c1e --- /dev/null +++ b/ts-packages/preview-runtime/src/vite-shims.d.ts @@ -0,0 +1,27 @@ +// Minimal type shims for bundler-specific import paths used in this +// package. Mirrors `vite/client` ambient declarations without forcing +// `vite` into preview-runtime's dependency tree. Vite (and Vitest, when +// running tests) handle the actual resolution at build/test time; this +// file just makes `tsc` happy. + +// `?url` suffix for asset imports (web-tree-sitter's WASM URL). +declare module '*.wasm?url' { + const url: string; + export default url; +} + +// The SASS JS bridge. Loaded both by the Rust WASM module (via +// `raw_module = "/src/wasm-js-bridge/sass.js"`) and by `wasmRenderer +// .ts`'s `setupSassVfsCallbacks`. The leading `/` is interpreted by +// Vite as a project-root-relative path, resolving to the consumer's +// `src/wasm-js-bridge/sass.js`. Every consumer (hub-client, the q2 +// preview SPA, …) must host these bridge files at that location. +declare module '/src/wasm-js-bridge/*.js' { + export function setVfsCallbacks( + readFn: (path: string) => string | null, + isFileFn: (path: string) => boolean, + listFn: () => string[], + ): void; + export function jsSassAvailable(): boolean; + export function jsSassCompilerName(): string; +} diff --git a/ts-packages/preview-runtime/src/wasm-quarto-hub-client.d.ts b/ts-packages/preview-runtime/src/wasm-quarto-hub-client.d.ts new file mode 100644 index 000000000..514ca4aa0 --- /dev/null +++ b/ts-packages/preview-runtime/src/wasm-quarto-hub-client.d.ts @@ -0,0 +1,151 @@ +/** + * Type declarations for wasm-quarto-hub-client + */ +declare module 'wasm-quarto-hub-client' { + export function init(): void; + export function vfs_add_file(path: string, content: string): string; + export function vfs_add_binary_file(path: string, content: Uint8Array): string; + export function vfs_remove_file(path: string): string; + export function vfs_list_files(): string; + export function vfs_clear(): string; + export function vfs_set_runtime_metadata(yaml: string): string; + export function vfs_get_runtime_metadata(): string; + export function vfs_read_file(path: string): string; + export function vfs_read_binary_file(path: string): string; + /** + * JS-interop user-grammar provider — hand-in to `render_qmd` / + * `render_qmd_content` so the render pipeline consults + * `web-tree-sitter`-backed grammars before built-ins. Construct via + * `new JsUserGrammars()`, populate via `register(class, fn)`, then + * pass the handle (or `undefined`). The handle is consumed by the + * render call; construct a fresh one per call. + */ + export class JsUserGrammars { + constructor(); + register( + language_class: string, + highlight_fn: (class_: string, source: string) => string | null | undefined, + ): void; + free(): void; + } + + export function render_qmd( + path: string, + user_grammars?: JsUserGrammars, + ): Promise; + export function render_qmd_content( + content: string, + template_bundle: string, + user_grammars?: JsUserGrammars, + ): Promise; + + /** Test-only: calls the user-grammar bridge directly. Phase 4.3 of syntax-highlighting. */ + export function quarto_highlight_with_user_for_test( + language_class: string, + source: string, + user: JsUserGrammars, + ): string | undefined; + export function get_builtin_template(name: string): string; + + // JavaScript execution test functions (interstitial validation) + export function test_js_available(): boolean; + export function test_js_simple_template(template: string, data_json: string): Promise; + export function test_js_ejs(template: string, data_json: string): Promise; + + // Project creation functions + export function get_project_choices(): string; + export function create_project(choice_id: string, title: string): Promise; + + // LSP intelligence functions + export function lsp_analyze_document(path: string): string; + export function lsp_get_symbols(path: string): string; + export function lsp_get_folding_ranges(path: string): string; + export function lsp_get_diagnostics(path: string): string; + + // QMD parsing and AST conversion functions + export function parse_qmd_content(content: string): string; + export function ast_to_qmd(ast_json: string): string; + /** Incrementally write a modified AST back to QMD, preserving unchanged source text. */ + export function incremental_write_qmd(original_qmd: string, new_ast_json: string): string; + + // Response type for parse/write operations + export interface AstResponse { + success: boolean; + /** JSON-serialized Pandoc AST (on successful parse) */ + ast?: string; + /** QMD source text (on successful AST-to-QMD conversion) */ + qmd?: string; + error?: string; + diagnostics?: AstDiagnostic[]; + } + + export interface AstDiagnostic { + kind: string; + title: string; + code?: string; + problem?: string; + hints: string[]; + start_line?: number; + start_column?: number; + end_line?: number; + end_column?: number; + details: { kind: string; content: string; start_line?: number; start_column?: number; end_line?: number; end_column?: number }[]; + } + + // SASS compilation functions + export function sass_available(): boolean; + export function sass_compiler_name(): string | undefined; + export function compile_scss(scss: string, minified: boolean, load_paths_json: string): Promise; + export function compile_scss_with_bootstrap(scss: string, minified: boolean): Promise; + export function compile_theme_css_by_name(theme_name: string, minified: boolean): Promise; + export function compile_default_bootstrap_css(minified: boolean): Promise; + + // Response types for project creation (for documentation/reference) + export interface ProjectChoice { + id: string; + name: string; + description: string; + } + + export interface ProjectChoicesResponse { + success: boolean; + choices: ProjectChoice[]; + } + + export interface ProjectFile { + path: string; + content_type: 'text' | 'binary'; + content: string; + mime_type?: string; + } + + export interface CreateProjectResponse { + success: boolean; + error?: string; + files?: ProjectFile[]; + } + + // Template processing functions + /** Process a template file: extract template-name and produce stripped content. */ + export function prepare_template(content: string): string; + + /** Response type for prepare_template */ + export type PrepareTemplateResponse = + | { + success: true; + /** The template-name metadata value, or null if not present */ + template_name: string | null; + /** The template content with template-name removed from frontmatter */ + stripped_content: string; + } + | { + success: false; + error: string; + }; + + export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + + export default function __wbg_init( + module_or_path?: InitInput | Promise + ): Promise; +} diff --git a/hub-client/src/services/wasmRenderer.test.ts b/ts-packages/preview-runtime/src/wasmRenderer.test.ts similarity index 100% rename from hub-client/src/services/wasmRenderer.test.ts rename to ts-packages/preview-runtime/src/wasmRenderer.test.ts diff --git a/hub-client/src/services/wasmRenderer.ts b/ts-packages/preview-runtime/src/wasmRenderer.ts similarity index 97% rename from hub-client/src/services/wasmRenderer.ts rename to ts-packages/preview-runtime/src/wasmRenderer.ts index 21df7aca0..13b5010e7 100644 --- a/hub-client/src/services/wasmRenderer.ts +++ b/ts-packages/preview-runtime/src/wasmRenderer.ts @@ -5,12 +5,14 @@ * VFS operations, QMD rendering, and SASS compilation. */ +/// + import type { Diagnostic, RenderResponse } from '@quarto/preview-renderer/types/diagnostic'; import type { RustQmdJson } from '@quarto/pandoc-types' import type { AstResponse } from 'wasm-quarto-hub-client' -import { discoverUserGrammars } from './userGrammarDiscovery'; -import { UserGrammarCache } from './userGrammarCache'; -import { loadUserGrammar } from './userGrammarHighlight'; +import { discoverUserGrammars } from './userGrammar/Discovery'; +import { UserGrammarCache } from './userGrammar/Cache'; +import { loadUserGrammar } from './userGrammar/Highlight'; // Response types from WASM module interface VfsResponse { @@ -152,8 +154,15 @@ export async function initWasm(): Promise { */ async function setupSassVfsCallbacks(): Promise { try { - // Import the sass bridge module - const sassModule = await import('../wasm-js-bridge/sass.js'); + // Import the sass bridge module. The same module is loaded by the + // Rust WASM at init-time via `wasm-bindgen`'s `raw_module = "/src/ + // wasm-js-bridge/sass.js"` annotation; using the same Vite-root + // absolute path here lets both consumers (Rust + TS) land on the + // same module instance regardless of which workspace package this + // file is bundled into. The consumer (hub-client today, the q2 + // preview SPA later) is required to host these bridge files at + // `src/wasm-js-bridge/`. + const sassModule = await import('/src/wasm-js-bridge/sass.js'); // Create VFS read callback const readFn = (path: string): string | null => { diff --git a/ts-packages/preview-runtime/vitest.config.ts b/ts-packages/preview-runtime/vitest.config.ts index 0fac223eb..d8d9d4d8f 100644 --- a/ts-packages/preview-runtime/vitest.config.ts +++ b/ts-packages/preview-runtime/vitest.config.ts @@ -12,6 +12,22 @@ export default defineConfig({ __dirname, '../../hub-client/wasm-quarto-hub-client/wasm_quarto_hub_client.js', ), + // Workspace-package aliases to source — mirrors the pattern in + // hub-client/vitest.config.ts. Vitest doesn't honor the `source` + // export condition on fresh clones, so workspace deps need + // explicit aliases when no `dist/` has been built. + '@quarto/quarto-automerge-schema': path.resolve( + __dirname, + '../quarto-automerge-schema/src/index.ts', + ), + '@quarto/quarto-sync-client': path.resolve( + __dirname, + '../quarto-sync-client/src/index.ts', + ), + '@quarto/preview-renderer': path.resolve( + __dirname, + '../preview-renderer/src', + ), }, }, test: { diff --git a/ts-packages/preview-runtime/vitest.integration.config.ts b/ts-packages/preview-runtime/vitest.integration.config.ts index c1ed5f837..a4ace14c6 100644 --- a/ts-packages/preview-runtime/vitest.integration.config.ts +++ b/ts-packages/preview-runtime/vitest.integration.config.ts @@ -9,6 +9,18 @@ export default defineConfig({ __dirname, '../../hub-client/wasm-quarto-hub-client/wasm_quarto_hub_client.js', ), + '@quarto/quarto-automerge-schema': path.resolve( + __dirname, + '../quarto-automerge-schema/src/index.ts', + ), + '@quarto/quarto-sync-client': path.resolve( + __dirname, + '../quarto-sync-client/src/index.ts', + ), + '@quarto/preview-renderer': path.resolve( + __dirname, + '../preview-renderer/src', + ), }, }, test: { From 5404bb6173b4f77322a963bcffcd65810df37eb8 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 10:48:02 -0500 Subject: [PATCH 009/108] docs(hub-client): changelog entry for bd-hfjj Phase 5 Co-Authored-By: Claude Opus 4.7 (1M context) --- hub-client/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/hub-client/changelog.md b/hub-client/changelog.md index 4f2bf0507..82bb3412a 100644 --- a/hub-client/changelog.md +++ b/hub-client/changelog.md @@ -15,6 +15,7 @@ be in reverse chronological order (latest first). ### 2026-05-13 +- [`9c508c91`](https://github.com/quarto-dev/q2/commits/9c508c91): Internal — move the WASM-renderer + automerge-sync + user-grammar services from `hub-client/src/services/` into the new `@quarto/preview-runtime` package, and move `iframePostProcessor` into `@quarto/preview-renderer` (bd-hfjj Phase 5; executed before Phase 4). Hub-client now imports these surfaces via `@quarto/preview-runtime` and `@quarto/preview-renderer/utils/iframePostProcessor`. No user-visible change. - [`7e45d2bf`](https://github.com/quarto-dev/q2/commits/7e45d2bf): Internal — move the rendering framework (Ast, dispatch, RegistryContext, plainText, meta, customNode) from `hub-client/src/components/render/framework/` into `@quarto/preview-renderer` (bd-hfjj Phase 3). Hub-client now imports the framework barrel through `@quarto/preview-renderer/framework`. No user-visible change. - [`5d8bd2b3`](https://github.com/quarto-dev/q2/commits/5d8bd2b3): Internal — start carving the preview pane into shared workspace packages (bd-hfjj Phase 2). Five type modules and seven utilities move from `hub-client/src/` into the new `@quarto/preview-renderer` package; hub-client now imports them via sub-paths. No user-visible change; this prepares for the `q2 preview` SPA reusing the exact same rendering code. From 6510577c6bec85d1f0b954f433511809597da05e Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 11:06:00 -0500 Subject: [PATCH 010/108] refactor(q2-preview): move q2-preview + iframe + overlays to preview-renderer (bd-hfjj Phase 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The biggest single move in the decomposition — 94 files touched. Lifts the q2-preview format components, the iframe wrappers, and the overlays out of hub-client and into @quarto/preview-renderer. To preview-renderer/src/q2-preview/ (entire subtree): AssetManifestContext, NoteNumberingContext, PreviewContext, PreviewDocument (+ integration test), assetWalker (+ test), dispatchers, entry (+ integration test), index.ts (barrel), quartoClasses, registry (+ test), theoremEnvs, utils, q2-preview.integration.test, custom-components.integration.test, + blocks/, inlines/, custom/ subtrees (each ~15-20 files). To preview-renderer/src/iframe/: Q2PreviewIframe (moved out of q2-preview/, + integration test), MorphIframe, DoubleBufferedIframe. To preview-renderer/src/overlays/: PreviewErrorOverlay (+ integration test), PreviewStaticInfoViews. `PreviewErrorOverlay` was DI-refactored as part of this move. Its previous internal `usePreference('errorOverlayCollapsed')` call coupled it to hub-client's localStorage layer; that wasn't viable inside the shared package. The component now takes optional `collapsed` + `onToggleCollapsed` props (controlled), with an internal `useState(true)` fallback for the uncontrolled case. The two hub-client call sites in `ReactPreview.tsx` and `Preview.tsx` wrap with `usePreference` and pass the value down — same UX, same persistence, cleaner boundary. The future q2-preview SPA can pass any state or omit the props entirely. Import wiring: - Self-package `@quarto/preview-renderer/` imports inside the moved subtree were converted to depth-aware relative paths via `/tmp/relativize-self-imports.py` (100 rewrites across 55 files). Idiomatic and matches the Phase 3 framework treatment. - `Q2PreviewIframe.tsx`'s `./assetWalker` import was rewritten to `../q2-preview/assetWalker` (it moved out of q2-preview into iframe/ as one of the few cross-subtree moves). - Hub-client importers of moved files were rewritten via `/tmp/rewrite-phase4-imports.py` (9 imports / 6 files in the first pass, plus 2 more after the regex was broadened to handle paths with intermediate `components/render/` segments). - One `vi.mock('./q2-preview/Q2PreviewIframe', ...)` in `ReactRenderer.integration.test.tsx` was retargeted to `'@quarto/preview-renderer/iframe/Q2PreviewIframe'`. Hub-client iframe-entry shim: `hub-client/q2-preview.html` has a ` + + +
      + + diff --git a/q2-preview-spa/index.html b/q2-preview-spa/index.html new file mode 100644 index 000000000..b5c6cb833 --- /dev/null +++ b/q2-preview-spa/index.html @@ -0,0 +1,24 @@ + + + + + + Quarto Preview + + + +
      + + + diff --git a/q2-preview-spa/package.json b/q2-preview-spa/package.json new file mode 100644 index 000000000..a941ecdac --- /dev/null +++ b/q2-preview-spa/package.json @@ -0,0 +1,27 @@ +{ + "name": "q2-preview-spa", + "private": true, + "version": "0.0.0", + "type": "module", + "description": "Standalone SPA host for Quarto's q2-preview format. Mirrors hub-client's preview pane by importing from @quarto/preview-renderer.", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "typecheck": "tsc -p tsconfig.app.json --noEmit", + "preview": "vite preview" + }, + "dependencies": { + "@quarto/preview-renderer": "*", + "@quarto/preview-runtime": "*", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vite-plugin-wasm": "^3.5.0" + } +} diff --git a/q2-preview-spa/src/main.tsx b/q2-preview-spa/src/main.tsx new file mode 100644 index 000000000..8ef2c1020 --- /dev/null +++ b/q2-preview-spa/src/main.tsx @@ -0,0 +1,38 @@ +/** + * Placeholder entry point for the q2-preview SPA. + * + * The standalone SPA is the future host of the `q2 preview` CLI command + * (bd-kw93). Today, its only job is to *prove the cross-package + * boundary works*: it imports a real component from + * `@quarto/preview-renderer` and renders something. That alone tells us + * the workspace plumbing, the `source` exports condition, and the + * extension resolution all line up. + * + * Phase A of bd-kw93 will replace this with the real wiring: samod sync + * client, WASM init, document-doc mounting, and `` + * driven off automerge state. + */ + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +// Import via the overlay sub-path rather than the top-level barrel. +// The barrel `@quarto/preview-renderer` re-exports the full q2-preview +// surface (which transitively pulls in `@quarto/preview-runtime` → +// `wasm-quarto-hub-client` + the `/src/wasm-js-bridge/*` glue). The SPA +// placeholder doesn't need any of that yet, and we don't yet host the +// bridge files at the SPA root. Phase A of bd-kw93 will revisit this +// when the SPA actually drives a render. +import { PreviewErrorOverlay } from '@quarto/preview-renderer/overlays/PreviewErrorOverlay'; + +const placeholder = { + message: + "q2 preview SPA — under construction. The CLI command `quarto preview` " + + "will boot this SPA against an ephemeral local hub. Today it just " + + "exercises the @quarto/preview-renderer boundary.", +}; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/q2-preview-spa/tsconfig.app.json b/q2-preview-spa/tsconfig.app.json new file mode 100644 index 000000000..c328724d3 --- /dev/null +++ b/q2-preview-spa/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/q2-preview-spa/tsconfig.json b/q2-preview-spa/tsconfig.json new file mode 100644 index 000000000..1ffef600d --- /dev/null +++ b/q2-preview-spa/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/q2-preview-spa/tsconfig.node.json b/q2-preview-spa/tsconfig.node.json new file mode 100644 index 000000000..a96b3e59e --- /dev/null +++ b/q2-preview-spa/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/q2-preview-spa/vite.config.ts b/q2-preview-spa/vite.config.ts new file mode 100644 index 000000000..5e58e6712 --- /dev/null +++ b/q2-preview-spa/vite.config.ts @@ -0,0 +1,45 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import wasm from 'vite-plugin-wasm'; +import path from 'path'; + +// The q2-preview SPA is the future host of `quarto preview` (bd-kw93). +// Today it's a placeholder that just confirms the @quarto/preview-renderer +// boundary is wired up. Phase A of bd-kw93 will fill it in with the real +// samod / WASM / preview-pane plumbing. +export default defineConfig({ + base: './', + plugins: [react(), wasm()], + resolve: { + // Prefer the `source` exports condition so workspace packages resolve + // straight to .ts/.tsx without requiring a pre-built dist. Mirrors + // hub-client/vite.config.ts. + conditions: ['source', 'import', 'module', 'browser', 'default'], + alias: { + // Mirror hub-client's wasm-quarto-hub-client alias. The SPA imports + // the same WASM module through the same `wasm-quarto-hub-client` + // bare specifier; pointing it at hub-client's symlink avoids + // duplicating the symlink for now. (When `quarto preview` formalizes + // the SPA build it can either keep this alias or copy the bridge + // dir to a SPA-local location.) + 'wasm-quarto-hub-client': path.resolve( + __dirname, + '../hub-client/wasm-quarto-hub-client/wasm_quarto_hub_client.js', + ), + }, + }, + optimizeDeps: { + exclude: ['wasm-quarto-hub-client', '@automerge/automerge'], + }, + build: { + target: 'esnext', + outDir: 'dist', + emptyOutDir: true, + }, + server: { + port: 5175, + fs: { + allow: ['..'], + }, + }, +}); From 06d03730b57ca7707ab313af0b28bc3dcc2b3769 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 11:19:42 -0500 Subject: [PATCH 013/108] docs(hub-client): changelog entry for bd-hfjj Phases 6+7 Co-Authored-By: Claude Opus 4.7 (1M context) --- hub-client/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/hub-client/changelog.md b/hub-client/changelog.md index e1d6b1524..e42362dd9 100644 --- a/hub-client/changelog.md +++ b/hub-client/changelog.md @@ -15,6 +15,7 @@ be in reverse chronological order (latest first). ### 2026-05-13 +- [`9e80360f`](https://github.com/quarto-dev/q2/commits/9e80360f): Internal — scaffold the new `q2-preview-spa` workspace package as the future host of the `quarto preview` CLI command, and extend `cargo xtask verify` to also build/test the new `@quarto/preview-renderer` + `@quarto/preview-runtime` packages and the SPA. Closes the bd-hfjj sub-epic. No user-visible change. - [`6510577c`](https://github.com/quarto-dev/q2/commits/6510577c): Internal — move the q2-preview format components (around 50 files), iframe wrappers, and overlay views from `hub-client/src/components/render/` into `@quarto/preview-renderer` (bd-hfjj Phase 4, the largest single move in the decomposition). The error-overlay was DI-refactored so it no longer reads `localStorage` directly; hub-client wraps with `usePreference` at the two call sites, preserving the existing UX. No user-visible change. - [`9c508c91`](https://github.com/quarto-dev/q2/commits/9c508c91): Internal — move the WASM-renderer + automerge-sync + user-grammar services from `hub-client/src/services/` into the new `@quarto/preview-runtime` package, and move `iframePostProcessor` into `@quarto/preview-renderer` (bd-hfjj Phase 5; executed before Phase 4). Hub-client now imports these surfaces via `@quarto/preview-runtime` and `@quarto/preview-renderer/utils/iframePostProcessor`. No user-visible change. - [`7e45d2bf`](https://github.com/quarto-dev/q2/commits/7e45d2bf): Internal — move the rendering framework (Ast, dispatch, RegistryContext, plainText, meta, customNode) from `hub-client/src/components/render/framework/` into `@quarto/preview-renderer` (bd-hfjj Phase 3). Hub-client now imports the framework barrel through `@quarto/preview-renderer/framework`. No user-visible change. From 8d95450f6ecf7d047ec4310455738d8b14f3dbac Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 11:52:57 -0500 Subject: [PATCH 014/108] docs(q2-preview): Phase A plan (bd-kw93 sub-epic kickoff) Drafts the engine-less CLI skeleton for `q2 preview` after bd-hfjj landed the decomposition. Seven sub-tasks (A.0 through A.6) plus an e2e smoke (A.7): A.0 Lift hub-client/src/wasm-js-bridge/ to a new @quarto/wasm-js-bridge workspace package; alias `/src/wasm- js-bridge` in hub-client + q2-preview-spa + preview-renderer tests. Resolves Q-A1 the right way (no duplication, no cross-platform symlink fragility). A.1 Stub `quarto preview` clap subcommand. A.2 New crates/quarto-preview/ shell crate with the quarto-trace-server-shaped include_dir! + build.rs placeholder. A.3 Fill in q2-preview-spa/src/main.tsx so the SPA actually drives a render. stays local for now. A.4 `cargo xtask build-q2-preview-spa` + wire into `build-all`. A.5 End-to-end boot test (tempdir lifecycle, samod handshake, URL-fragment carrier). A.6 Always-visible "force re-render" button (epic resolution #4). A.7 Playwright smoke (real-browser flow incl. DOM-stability invariant). Q-A3 source-code audit corrected an inherited claim: the epic and my initial draft both said the SPA reads `indexDocId`/`wsUrl` from a server-injected `` tag "the same trick share links use." Both halves wrong. hub-client's share links use URL *fragments* (`hub-client/src/App.tsx:272`, `utils/routing.ts`); there's no meta-injection in the codebase. trace-server serves `index.html` unmodified (`crates/quarto-trace-server/src/lib.rs:185-187`). Phase A now uses the same URL-fragment pattern: the CLI opens `http://:/#/preview/`, the SPA reads `window.location.hash`, the WebSocket URL derives from `window.location`. No new server-side patterns introduced. Q-A5 captures a future direction: persistent samod storage inside `.quarto/preview/samod/` to amortize the "blank screen at boot" cost on large projects. Phase A measures the cost; future phases act. TDD: every sub-task is structured tests-first. Status: approved 2026-05-13 (all open questions resolved); ready to file beads sub-issues and begin A.0. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-05-13-q2-preview-phase-a.md | 428 ++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 claude-notes/plans/2026-05-13-q2-preview-phase-a.md diff --git a/claude-notes/plans/2026-05-13-q2-preview-phase-a.md b/claude-notes/plans/2026-05-13-q2-preview-phase-a.md new file mode 100644 index 000000000..cf2d7db18 --- /dev/null +++ b/claude-notes/plans/2026-05-13-q2-preview-phase-a.md @@ -0,0 +1,428 @@ +--- +date: 2026-05-13 +branch: beads/bd-???-q2-preview-phase-a (TBD — sub-issue of bd-kw93) +beads: TBD (file as sub-issue of bd-kw93 after this plan is reviewed) +status: approved 2026-05-13 (Q-A1 through Q-A5 resolved); ready to file beads sub-issues and begin A.0 +--- + +# `q2 preview` — Phase A (engine-less CLI skeleton) + +## Goal + +Ship the smallest end-to-end vertical slice of `q2 preview` that +proves the architecture in +[`2026-05-11-q2-preview-epic.md`](2026-05-11-q2-preview-epic.md) works: + +``` +user runs `q2 preview foo.qmd` + → CLI spins up an ephemeral quarto-hub server with a temp data_dir + → opens a browser at the served URL + → the q2-preview SPA loads, connects via samod ws to the hub + → the SPA renders foo.qmd (and the rest of the project) through + WASM + → user edits foo.qmd on disk → FileWatcher syncs → SPA re-renders + incrementally + → Ctrl-C tears it all down, deletes the temp dir +``` + +**Out of scope for Phase A:** engine execution (any document with +`{r}` / `{python}` / `{ojs}` code cells renders the source as-is in +this phase; Phase C wires replay). PDF, multi-window, sharing, freeze +— all later. + +## What changed since the epic's Phase A draft + +The epic (2026-05-11) anticipated A.3 as "add a `hub-client` preview- +mode build target." bd-hfjj (2026-05-13) decomposed hub-client into +shared workspace packages and created `q2-preview-spa/` as a sibling +workspace package — exactly the SPA the preview CLI needs. So A.3 +**becomes**: "fill in `q2-preview-spa/src/main.tsx` enough that the +served bundle actually drives a render." + +The `crates/quarto-preview/` build.rs pattern (A.2 + A.4) is +unchanged — `quarto-trace-server/build.rs` remains the precedent. + +## Phasing inside Phase A + +Seven sub-tasks (A.0 → A.6) plus an e2e smoke (A.7), each executable +independently. Each ends in a green `cargo xtask verify` (now 11 +steps after bd-hfjj). The TDD policy applies — tests precede every +implementation step. + +### A.0 — Lift the WASM JS bridge to `@quarto/wasm-js-bridge` + +(Decision: Q-A1 option (c) — make this a proper workspace package +*now* rather than carry a duplicate or symlink forward.) + +**Why this is first:** A.3 (filling in the SPA) requires the SPA's +Vite root to host `/src/wasm-js-bridge/*` (the WASM module's +`raw_module = "/src/wasm-js-bridge/sass.js"` path resolves +project-root-relative). Doing it as the first sub-task means A.3 +inherits a clean answer. + +**Tests first:** +- Existing hub-client `test:wasm` suite continues to render the + changelog and run the smoke-all fixtures unchanged (the bridge + was the dynamic-import target wasmRenderer.ts used post-Phase 5; + if alias resolution is wrong, sass compilation breaks and + hub-client's theme/smoke wasm tests fail). +- New `ts-packages/wasm-js-bridge/src/sass.test.ts` (light) — + imports the package's `sass.js` and asserts `jsSassAvailable` / + `setVfsCallbacks` are functions. Just guards the public API + against accidental rename or removal. + +**Implementation:** +- Create `ts-packages/wasm-js-bridge/`: + - `package.json` (name `@quarto/wasm-js-bridge`, `private: true`, + `type: "module"`, no build scripts — the files are loaded + directly by Vite at each consumer). + - `src/sass.js`, `sass.d.ts`, `cache.js`, `cache.d.ts`, + `fetch.js`, `template.js` — `git mv`'d from + `hub-client/src/wasm-js-bridge/`. +- Register the new workspace in root `package.json`'s + `workspaces`. +- Consumer wiring — `hub-client/vite.config.ts`, + `hub-client/vitest.{,integration,wasm}.config.ts`, and + `q2-preview-spa/vite.config.ts` all add: + ```ts + alias: { + '/src/wasm-js-bridge': path.resolve(__dirname, + '../ts-packages/wasm-js-bridge/src'), + } + ``` + (For hub-client the path is `../ts-packages/...`; for the SPA + it's `../ts-packages/...` from `q2-preview-spa/`.) `wasm-bindgen`'s + `raw_module = "/src/wasm-js-bridge/sass.js"` is unchanged — the + alias rewrites where `/src/wasm-js-bridge/` resolves at consumer + build time. +- Same alias added to `ts-packages/preview-renderer/vitest.integration.config.ts` + (already has a similar one pointing at hub-client's copy from + Phase 4 — retarget it to the new package). Drop the hub-client + fallback. +- Remove `hub-client/src/wasm-js-bridge/` (after `git mv`'s done + its work — the directory should be empty). + +**Acceptance:** +- `cargo xtask verify` green on all 11 steps. Especially + `hub-client test:wasm` (which exercises the bridge through real + sass compilation) and `preview-renderer test:integration`. +- The dir `hub-client/src/wasm-js-bridge/` no longer exists. +- The bridge files have only one home in the workspace. + +### A.1 — Wire up the empty `q2 preview` clap subcommand + +**Tests first:** +- `crates/quarto/tests/preview_cli.rs` — invokes `quarto preview --help` + via `assert_cmd`, asserts the args list (`[path]`, `--port`, + `--no-browser`, `--data-dir`, `--preview-dir`) and an exit code 0. + Confirms the subcommand is registered before any of it does anything. + +**Implementation:** +- New `crates/quarto/src/commands/preview.rs` mirroring `commands/hub.rs`'s + shape (`PreviewArgs` struct + `execute(args) -> Result<()>`). +- `execute()` is a stub that prints "preview not implemented yet" and + exits — A.5 wires it to actually boot. +- Add `Preview(...)` variant to the `Command` enum in `main.rs`; route + to `commands::preview::execute`. + +**Acceptance:** `quarto preview --help` shows the expected args. +`quarto preview` exits 0 with the stub message. `cargo xtask verify` +green. + +### A.2 — Create `crates/quarto-preview/` shell crate + +A new workspace crate that owns the CLI logic + the embedded SPA + +preview-specific axum routes. `commands/preview.rs` in the `quarto` +binary becomes a thin shim that builds `PreviewConfig` and calls +`quarto_preview::run(config)`. + +**Tests first:** +- `crates/quarto-preview/tests/smoke.rs` — constructs a `PreviewConfig` + with `--no-browser` and a known port; spawns `run()` on a tokio + runtime; awaits the "server listening" log line via a channel; hits + `GET /` over HTTP; asserts 200 and that the body looks like the SPA's + `index.html` (contains `
      `). Tears the server down. + + This is the "fewest-moving-parts integration test" — it covers + routing, embedding, and lifecycle without engines or the browser. + +**Implementation:** +- `crates/quarto-preview/Cargo.toml` — depends on `quarto-hub`, + `include_dir`, `axum`, `tokio`. +- `crates/quarto-preview/src/lib.rs` — `pub async fn run(config: + PreviewConfig) -> Result<()>` that: + 1. constructs `HubConfig` (project mode by default; standalone if + `--no-project` per A.5), + 2. builds the hub's axum router via existing `quarto_hub` API, + 3. layers a preview-specific fallback that serves the embedded SPA + bundle on any non-API path, + 4. spawns the server. +- `crates/quarto-preview/build.rs` — copies trace-viewer's pattern + *exactly*: look for `q2-preview-spa/dist/index.html`; if present, + embed; otherwise emit a placeholder into `OUT_DIR` saying "run + `cargo xtask build-q2-preview-spa`." +- `include_dir!("$QUARTO_PREVIEW_EMBED_DIR")` in lib.rs. + +**Acceptance:** smoke test above passes. `cargo build -p quarto-preview` +succeeds with or without `q2-preview-spa/dist/` existing. + +### A.3 — Fill in `q2-preview-spa/src/main.tsx` + +The placeholder created in bd-hfjj Phase 6 just renders +`` with static text. It needs to become a real +preview host. Minimum viable shape: + +``` +main.tsx +├── reads indexDocId + wsUrl from a tag (server emits these) +├── calls initWasm() ──── @quarto/preview-runtime +├── connect(wsUrl, indexDocId, ...) ──── @quarto/preview-runtime +│ → loads project files into the VFS via the existing +│ wasmRenderer.vfsAdd* callbacks +├── picks an "active file" from the URL hash or first .qmd in the +│ project +└── renders with the active file's content, + re-rendering on automerge changes. +``` + +This is the "engine-less render" path. Code cells appear as source +(matching Phase A's scope). Phase C will wire `EngineCapture` here. + +**Tests first:** +- `q2-preview-spa/src/main.integration.test.tsx` (new) — mounts the SPA + with a mock SyncClient (from `@quarto/preview-runtime/test-utils/mockSyncClient`) + and a mock WasmRenderer (from `@quarto/preview-runtime/test-utils/mockWasm`), + verifies it renders a basic .qmd through ``. Mirrors the + approach hub-client uses for its preview pane integration tests. +- Vitest's `vitest.integration.config.ts` for q2-preview-spa, configured + with the same WASM aliases that bd-hfjj already proved work in the + preview-renderer + preview-runtime packages. + +**Implementation:** +- New `src/services/connection.ts` in q2-preview-spa that wraps + `@quarto/preview-runtime`'s `connect()` and exposes file state to + the React tree (`useSyncedFiles()` hook or similar — the simpler + shape, not a full Editor-level subscription). +- `main.tsx` mounts a tiny `` component: + - if WASM not ready: render `` with "Initializing…" + - if connection error: render `` with the error + - otherwise: render `` + where the AST comes from a `renderPageInProject()` call on every + relevant automerge change. +- Bridge resolution is taken care of by A.0 — the SPA's + `vite.config.ts` aliases `/src/wasm-js-bridge` to + `@quarto/wasm-js-bridge`'s src. No per-consumer copy of the bridge + files. + +**Acceptance:** +- The integration test passes. +- `cd q2-preview-spa && npm run dev` — manually point a browser at + `localhost:5175` (with a static `` tag for testing). The SPA + loads, WASM inits, the placeholder file renders. +- End-to-end via A.5 once the CLI boots. + +### A.4 — Build orchestration: `cargo xtask build-q2-preview-spa` + +Mirror `cargo xtask build-trace-viewer` (see `crates/xtask/src/build_trace_viewer.rs`). + +**Tests first:** none directly — this is plumbing. Manual: +`cargo xtask build-q2-preview-spa` succeeds; `q2-preview-spa/dist/` +exists. + +**Implementation:** +- New `crates/xtask/src/build_q2_preview_spa.rs`. +- Extend `cargo xtask build-all` to chain the SPA build before + `cargo build` so the embedded `include_dir!` picks it up. + +**Acceptance:** `cargo xtask build-all` produces a `quarto` binary +that includes the real SPA bundle (verifiable by `quarto preview` +serving the dashed-filename `index-.js` from the SPA's +prod build). + +### A.5 — `q2 preview` boots end-to-end + +This is the integration step that ties A.1–A.4 together. + +**Tests first:** +- `crates/quarto-preview/tests/boot.rs` — spawns `q2 preview --no-browser + --port 0` against a fixture project (tempdir with a single `foo.qmd` + containing `# Hello`); asserts: + - The server's HTTP endpoint serves the SPA's `index.html`. + - The hub's websocket endpoint accepts a samod handshake. + - The temp `data_dir` is created at startup and *deleted* on + shutdown (drop the runtime; check the dir is gone). + - The launch URL the CLI computes is shaped `http://:/#/preview/` + (URL-fragment carrier, per Q-A3). + +**Implementation:** +- `commands/preview.rs::execute()` becomes: + 1. Resolve `path` arg (default = cwd if it has `_quarto.yml`, + otherwise current file). + 2. Construct a temp `data_dir` via `tempfile::TempDir` (so it's + `Drop`-deleted on shutdown automatically). + 3. Build `HubConfig` in project mode (or standalone if `--no-project`). + 4. Call `quarto_preview::run(config).await`. + 5. On the first "server listening" log, compute the launch URL as + `http://:/#/preview/` and optionally + open it in the browser (unless `--no-browser`). + 6. Ctrl-C handler that stops the server and lets `TempDir` clean up. +- **No server-side HTML rewriting.** The SPA reads `indexDocId` from + `window.location.hash` (matching the existing `#/share/...` route + pattern in `hub-client/src/utils/routing.ts`). The WebSocket URL is + derived client-side from `window.location` — + `ws://:/ws`. See Q-A3 below. + +**Acceptance:** the boot test passes. Manual run: `quarto preview` in +a Quarto project pops a browser tab, the preview pane renders, +`Ctrl-C` cleans up. Per CLAUDE.md §End-to-end verification, record +the inspection. + +### A.6 — Manual force-refresh button + +Per the epic's resolution #4 (force-refresh invariant): the preview +UI *always* offers a "re-render now" button as the user's escape +hatch when the dependency-graph misses a cross-doc relationship. + +**Tests first:** +- `q2-preview-spa/src/components/ForceRefresh.integration.test.tsx` + (new) — renders the button, clicks it, verifies the mocked + `renderPageInProject` is called. + +**Implementation:** +- A small persistent control in the SPA's chrome (corner of the + iframe; doesn't intrude on the rendered content). +- Click handler re-invokes the SPA's render path against current + automerge state. No server roundtrip in Phase A (engines are out of + scope); Phase C extends to trigger server-side re-execution when + applicable. + +**Acceptance:** test passes; manual button click in `npm run dev` +re-runs the render. + +### A.7 — End-to-end smoke test (Playwright) + +The integration tests above cover the pieces. A.7 covers the +**human-shaped path**. + +**Tests first:** +- `q2-preview-spa/e2e/basic-preview.spec.ts` (new) — Playwright + config that spawns `quarto preview` against a test fixture, opens + the served URL in chromium, asserts: + - The preview renders (DOM contains expected `# Hello` text). + - Editing the fixture `.qmd` on disk produces a visible content + change in the iframe within 2s. + - The force-refresh button works. + - The Q2-preview format's DOM-stability invariant holds across an + edit: a `data-stable-id` element keeps the same DOM node identity + (using Playwright's `evaluate()` to grab the node pre- and + post-edit and `===` compare them). + +**Acceptance:** the e2e test runs in CI (extends to `--include-e2e` +shape on `cargo xtask verify`). + +## Tradeoffs / Open questions + +### Q-A1 — bridge files duplication vs symlink + +**Decided 2026-05-13: option (c)** — lift to `@quarto/wasm-js-bridge` +workspace package now. Captured as sub-task A.0 above. + +Wiring detail: each consumer's vite config aliases +`/src/wasm-js-bridge` to the new package's `src/`. The Rust WASM +module's `raw_module = "/src/wasm-js-bridge/sass.js"` annotation +stays unchanged; the alias controls where consumers resolve that +path. No per-consumer shim files needed. + +### Q-A2 — How aggressively to extract `` shape + +main.tsx in A.3 is described as "tiny" with an inline ``. +Could grow. Alternative is to ship `` as a *reusable +component* in `@quarto/preview-renderer` (so hub-client could +eventually also share this orchestration logic). That's a bigger +move than A.3 needs, but it would mean hub-client and the SPA share +not just the rendering primitives (Phase 4 of bd-hfjj) but also the +orchestration (state, connection, dispatch). + +Lean: **keep `` inside q2-preview-spa for Phase A.** If +hub-client decides it wants to share orchestration too, that's a +later refactor. + +### Q-A3 — How does the SPA learn `indexDocId` + `wsUrl`? + +**Resolved 2026-05-13 (after source-code audit invalidated the +original premise):** the SPA reads `indexDocId` from +`window.location.hash`. `wsUrl` is derived client-side from +`window.location` as `ws://:/ws`. The CLI just opens +`http://:/#/preview/` in the browser. + +History: the epic plan (and my original Phase A draft) claimed +"reads from a `` tag emitted by the server (same trick share +links use)." That was wrong on both counts — hub-client share links +use URL fragments, not meta tags, and there's no meta-injection +anywhere in this codebase. The audit traces: + +- `hub-client/src/App.tsx:272` — routes on `route.type === 'share'` + with `#/share/...` URL fragments. +- `hub-client/src/utils/routing.ts` — fragment parser. +- `crates/quarto-trace-server/src/lib.rs:185-187` — serves the + embedded `index.html` *unmodified*; SPA reads its config from a + Vite-build-time env var. + +URL-fragment carrier wins because: zero server-side rewriting (no +new axum middleware), matches the existing share-link pattern, and +fragments are the conventional place to carry client-side route +state in SPAs. + +### Q-A4 — Browser-open behavior + +Default: open `http://localhost:/` in the system default browser +after the server reports "listening." Override with `--no-browser`. + +Lean: use the `open` crate (small, cross-platform). The trace-server +does something similar (check). + +### Q-A5 — Initial sync time on large projects + +For a project with 500+ files, `HubContext::new` pushes them all into +automerge at startup. The blank-screen wait is real. Phase A doesn't +need to solve this, but should *measure* it — a smoke test on a +30-file fixture is fine, but record what 500 looks like. + +Lean: time the boot on a representative fixture; if it's >2s, +file a perf follow-up for Phase B. + +**Future direction (user note, 2026-05-13):** worth considering +persistent samod storage *inside `.quarto/`* (e.g. +`.quarto/preview/samod/`). On startup, sync the existing +filesystem-backed store against the project (fresh-create when +missing). This turns the "blank screen at boot" into a one-time +cost per project rather than every invocation. Not a Phase-A +concern — note here so the perf numbers from this phase inform the +decision. + +## Beads issues to file after plan approval + +Sub-issues of bd-kw93, in execution order: +1. **(A.0)** Lift WASM JS bridge to `@quarto/wasm-js-bridge` + workspace package; switch hub-client + the SPA (and preview-renderer + tests) to alias-resolve through it. +2. **(A.1 + A.2)** `quarto preview` CLI shim + `crates/quarto-preview/` + shell crate with the trace-server-shaped `include_dir!` + + `build.rs` placeholder. +3. **(A.3)** Fill in `q2-preview-spa/src/main.tsx` so the SPA boots + through samod + WASM and drives a real render. +4. **(A.4)** `cargo xtask build-q2-preview-spa` + wire it into + `build-all`. +5. **(A.5)** End-to-end boot integration test (`q2 preview` + spins everything up against a fixture project). +6. **(A.6)** Manual force-refresh button. +7. **(A.7)** Playwright smoke test in `q2-preview-spa/e2e/`. + +## Reference + +- Epic: `claude-notes/plans/2026-05-11-q2-preview-epic.md` +- Decomposition (just landed): `claude-notes/plans/2026-05-11-hub-client-decomposition.md` +- CLI subcommand model: `crates/quarto/src/commands/hub.rs` +- `include_dir!` precedent: `crates/quarto-trace-server/{build.rs, src/lib.rs}` +- `cargo xtask build-trace-viewer` model: `crates/xtask/src/build_trace_viewer.rs` +- Hub server entry: `crates/quarto-hub/src/server.rs` From 1e6c15d608ca4da9254cff719b424095fc938169 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Wed, 13 May 2026 11:55:41 -0500 Subject: [PATCH 015/108] sync beads: file 7 Phase A sub-issues under bd-kw93 Files bd-0xmt (A.0 wasm-js-bridge), bd-yxqt (A.1+A.2 CLI + crate), bd-o5wd (A.3 SPA main.tsx), bd-501n (A.4 xtask build), bd-mflk (A.5 boot test), bd-b5hf (A.6 force-refresh), bd-vpsy (A.7 Playwright) as parent-child children of bd-kw93. Plan: claude-notes/plans/2026-05-13-q2-preview-phase-a.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- .beads/issues.jsonl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index e674b8496..db046f8d3 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -5,6 +5,7 @@ {"id":"bd-0jyl","title":"Source-info threading through listing markdown re-parse","description":"L3's ListingRenderTransform invokes pampa::readers::qmd::read on doctemplate output to obtain Pandoc blocks, then discards the fresh SourceContext. Diagnostics from that re-parse are collapsed into a single Q-12-10 host-page warning naming the listing id and first message — not threaded back to the host page's SourceContext.\n\nFor a proper design, a way is needed to merge the fresh SourceContext from the re-parse into the host page's SourceContext, *and* preserve the chain 'host YAML key → template substitution → markdown span' through to the diagnostic. Likely cuts across quarto-source-map, quarto-doctemplate, and the diagnostic-builder layer. Worth its own design session.\n\nDiscovered while writing L3 (bd-ml8z) sub-plan; see D3 in claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-06T18:26:24.690377Z","created_by":"cscheid","updated_at":"2026-05-06T18:26:24.690377Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-0jyl","depends_on_id":"bd-ml8z","type":"discovered-from","created_at":"2026-05-06T18:26:24.690377Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-0tr6","title":"Website projects epic: multi-page sites with sidebars, shared resources, and hub-client integration","description":"Design and implement multi-page website projects for Quarto 2. Covers: typed static-document snapshots (PageSnapshot/DocumentProfile working name), pipeline resumability, ProjectType trait, website sidebars, scoped/relocatable artifact stores (site_libs), cross-doc link rewriting, sitemap/favicon/site-url, incremental rebuilds, and hub-client project rendering.\n\nMVP scope explicitly excludes: search, listings/RSS, aliases, announcements, analytics, 404, reader-mode, repo-actions, breadcrumbs, book project type, quarto preview, freeze.\n\nPlan: claude-notes/plans/2026-04-23-website-project-epic.md\n\nDesign decisions from initial conversation:\n- Snapshot is a typed serializable value produced at a pipeline checkpoint after MetadataMergeStage; downstream code consumes Vec; user filters read but do not mutate.\n- Pipeline stages up to snapshot are resumable (cloneable) to avoid redundant execution across pass 1 (snapshot sweep) and pass 2 (per-file render).\n- ProjectType trait introduced now (website, default). Single-document renders go through DefaultProjectType.\n- Artifact stores scope-aware (Page vs Project) and relocatable; single-doc = project with implicit _quarto.yml.\n- Hub-client caches project nav state; renders active page with cached state. Design must leave room for Q2 'quarto preview' which will be a local hub-client instance.\n- Naming TBD in Phase 0 (DocumentProfile working name; user dislikes Page*/Metadata*).","status":"open","priority":1,"issue_type":"epic","created_at":"2026-04-23T18:42:28.206394Z","created_by":"cscheid","updated_at":"2026-04-23T18:42:28.206394Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-0wyo","title":"Server-precomputed other_metadata_html for default listing","description":"Q1's item-default.ejs.md iterates over fields *not* in the curated set ('title','image','image-alt','date','author','subtitle','description','reading-time','categories') and emits a per such field. This is dynamic in EJS — there is no equivalent in doctemplate without server-side precomputation.\n\nL3 v1 ships the default listing with curated-fields-only output. This issue picks up the gap by adding an other_metadata_html string to the per-item TemplateValue::Map binding, computed server-side in the listing render transform. The built-in item-default.template gets one more interpolation site: $item.other-metadata-html$.\n\nSource-code TODO marker lands in crates/quarto-core/src/project/listing/templates/item-default.template at the otherFields gap location (added during L3).\n\nDiscovered while writing L3 (bd-ml8z) sub-plan; see D15 in claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-06T18:26:31.323613Z","created_by":"cscheid","updated_at":"2026-05-06T18:26:31.323613Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-0wyo","depends_on_id":"bd-ml8z","type":"discovered-from","created_at":"2026-05-06T18:26:31.323613Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-0xmt","title":"Phase A.0: Lift WASM JS bridge to @quarto/wasm-js-bridge workspace package","description":"Move hub-client/src/wasm-js-bridge/ to a new @quarto/wasm-js-bridge workspace package. Wire hub-client + q2-preview-spa + preview-renderer test configs to alias /src/wasm-js-bridge -> the package's src. Removes the cross-platform symlink/duplication risk for Phase A.3. See claude-notes/plans/2026-05-13-q2-preview-phase-a.md §A.0.","status":"open","priority":1,"issue_type":"task","created_at":"2026-05-13T16:53:11.353454Z","created_by":"cscheid","updated_at":"2026-05-13T16:53:11.353454Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-0xmt","depends_on_id":"bd-kw93","type":"parent-child","created_at":"2026-05-13T16:53:11.353454Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-1066","title":"HTML comments lost during incremental writes","description":"HTML comments (<\\!-- ... -->) are dropped from the Pandoc AST during parsing. tree-sitter parses them as 'comment' nodes which become IntermediateUnknown and are silently removed. This causes comments to be lost when the incremental writer rewrites blocks containing comments. Block-level standalone comments survive as empty Para blocks (preserved via source spans on KeepBefore), but inline comments within paragraphs are completely gone.","status":"open","priority":1,"issue_type":"bug","created_at":"2026-02-09T15:35:05.022976Z","created_by":"cscheid","updated_at":"2026-02-09T15:35:05.022976Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-11a1","title":"Automerge-backed project set storage","description":"Replace per-browser IndexedDB project list with a synced Automerge document. See claude-notes/plans/2026-03-31-automerge-project-set.md","status":"open","priority":1,"issue_type":"epic","created_at":"2026-03-31T20:37:29.036441Z","created_by":"cscheid","updated_at":"2026-03-31T20:37:29.036441Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-128x","title":"Slide renderer crashes on empty slides document","description":"When a document has format: q2-slides but no slide content (no headers, no title), parseSlides() returns an empty array. SlideAst then calls renderSlide(slides[0]) which is undefined, crashing on slide.type access. The fix should handle the empty slides case gracefully by showing an empty/placeholder slide. Plan: claude-notes/plans/2026-02-26-empty-slides-crash.md","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-02-26T15:13:14.778662Z","created_by":"cscheid","updated_at":"2026-02-26T16:34:51.200462Z","closed_at":"2026-02-26T16:34:51.200439Z","close_reason":"Fixed: guard renderSlide call and hide nav controls when slides array is empty","source_repo":".","compaction_level":0,"original_size":0} @@ -74,6 +75,7 @@ {"id":"bd-4g6g","title":"[websites epic] Move sidebar to Q1 template position (sidebar-left)","description":"Phase 2 puts the sidebar beside the TOC on the right — minimum churn in the existing FULL_HTML_TEMPLATE. Q1 renders sidebar-left, TOC-right. Moving to the Q1 layout is a template restructuring task: change the two-column grid, adjust layout CSS, and re-verify any layout-sensitive tests. Separate from sidebar feature work.\n\nPlan reference: claude-notes/plans/2026-04-24-websites-phase-2.md Decision 4 + follow-up.","status":"open","priority":3,"issue_type":"task","created_at":"2026-04-24T17:53:05.037896Z","created_by":"cscheid","updated_at":"2026-04-24T17:53:05.037896Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-4g6g","depends_on_id":"bd-9svl","type":"discovered-from","created_at":"2026-04-24T17:53:05.037896Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-4ho9","title":"L9 follow-up: validate against W3C feed validator","description":"L9 v1 ships snapshot tests + manual end-to-end inspection. File any parse warnings raised by canonical RSS validators on representative outputs and fix. Validators to try: W3C feed validator (online), feedvalidator.org's offline tool, Pandoc's own RSS schema if available.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-08T17:33:22.925543Z","created_by":"cscheid","updated_at":"2026-05-08T17:33:22.925543Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-4ho9","depends_on_id":"bd-o90m","type":"discovered-from","created_at":"2026-05-08T17:33:22.925543Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-4zdf","title":"Draft-mode interaction with sitemap","description":"Phase 7 emits sitemap entries for every profiled page including drafts. When draft-mode YAML config lands (see bd-p4sc and friends from Phase 6), gate the sitemap emission so draft pages are omitted unless draft-mode == \"visible\". Originating phase: bd-b9mz; coordinate with bd-p4sc.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-04-27T15:03:22.529546Z","created_by":"cscheid","updated_at":"2026-04-27T15:03:22.529546Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-4zdf","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-27T15:03:22.529546Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-4zdf","depends_on_id":"bd-b9mz","type":"discovered-from","created_at":"2026-04-27T15:03:22.529546Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-4zdf","depends_on_id":"bd-p4sc","type":"related","created_at":"2026-04-27T15:03:22.529546Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-501n","title":"Phase A.4: cargo xtask build-q2-preview-spa + build-all wiring","description":"New crates/xtask/src/build_q2_preview_spa.rs (mirror build_trace_viewer). Extend build-all to chain the SPA build before cargo build so include_dir! picks up the real bundle. See claude-notes/plans/2026-05-13-q2-preview-phase-a.md §A.4.","status":"open","priority":1,"issue_type":"task","created_at":"2026-05-13T16:53:28.180875Z","created_by":"cscheid","updated_at":"2026-05-13T16:53:28.180875Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-501n","depends_on_id":"bd-kw93","type":"parent-child","created_at":"2026-05-13T16:53:28.180875Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-56b0","title":"Cross-doc dependency channel audit for q2 preview","description":"Enumerate every Quarto feature that creates a cross-document dependency the preview pipeline needs to react to: include shortcodes, listing content globs, bibliography/csl paths, theme SCSS imports, project-scoped resources, _extensions/ Lua filters, and anything else. For each: which DocumentProfile channel encodes it today (if any), which need to be added, which stay manual-refresh-only. Drives Phase B (channels already on DocumentProfile) and informs Phase D (new channels). Until this lands, the always-visible manual force-refresh button (Phase A.6) is the user escape hatch. See claude-notes/plans/2026-05-11-q2-preview-epic.md §Recommended next steps item 3 + Risk 2.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-11T15:40:54.896280Z","created_by":"cscheid","updated_at":"2026-05-11T15:40:54.896280Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-56b0","depends_on_id":"bd-kw93","type":"related","created_at":"2026-05-11T15:40:54.896280Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-57y4","title":"Vendor and integrate quarto-listing.scss with theme-CSS pipeline","description":"Per L3 D5 (decided 2026-05-06), the listing JS pair (list.min.js + quarto-listing.js) shipped via the artifact store as Project-scoped js: artifacts in the L3 phase 7 commit. The third asset, quarto-listing.scss, was deferred because it requires Bootstrap variable wiring + media-breakpoint mixins from the existing theme-CSS pipeline (CompileThemeCssStage / quarto_sass::SassLayer). That's a larger design task than the static JS bundling.\n\nThis issue: integrate the listing SCSS as a SassLayer that gets composed with the website's theme bundle. The Q1 source file lives at external-sources/quarto-cli/src/resources/projects/website/listing/quarto-listing.scss; copy to resources/listing/quarto-listing.scss per the External Sources Policy and add it to the theme bundle when at least one listing is rendered.\n\nWhile the SCSS isn't compiled, listings render with default browser styling — the markup and JS are unchanged. The list / grid / table layouts work but lack the polished card / table styling Q1 ships.\n\nDiscovered while shipping L3 phase 7 (bd-ml8z); see D5 in claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md.\n\nL5 (bd-5vsr) lands the markup that consumes this SCSS; merging bd-57y4 restores Q1 visual parity for category sidebars and per-item category chips.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-06T20:52:15.894830Z","created_by":"cscheid","updated_at":"2026-05-07T13:46:18.157025Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-57y4","depends_on_id":"bd-ml8z","type":"discovered-from","created_at":"2026-05-06T20:52:15.894830Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-5qnj","title":"Manage trace size for use as replay/regression-test fixtures","description":"Spun out of bd-45yw (replay engine). Once traces double as replay fixtures (and as user-attached bug-report artifacts), trace size becomes a real constraint — they will be checked into the repo as regression fixtures and posted by users in issues.\n\nCurrent quarto-trace output captures per-stage pipeline state (see crates/quarto-trace/src/lib.rs and crates/quarto-core/src/stage/trace.rs::JsonTraceObserver), which is already on the heavy side for routine diagnostic use.\n\nInvestigate:\n- Where the bulk lives in current traces (per-stage AST snapshots? supporting_files content? something else?)\n- Which content is actually load-bearing for replay vs. diagnostics, and whether the two roles can share one artifact (the design preference noted in bd-45yw's plan)\n- Compression on disk; lazy loading on read\n- Whether per-stage AST snapshots can be elided/diffed when not needed\n- Size budgets — what is reasonable for (a) checked-in CI fixtures and (b) user-attached bug reports\n\nGoal: make 'one trace serves both diagnostic and replay roles' practical. If size cannot be bounded, fall back to two separate artifacts and document why.\n\nReferences:\n- crates/quarto-trace/src/lib.rs (TraceDocument, TraceEntry)\n- crates/quarto-core/src/stage/trace.rs (JsonTraceObserver)\n- claude-notes/plans/2026-05-03-replay-engine.md (open subquestion in Phase 1)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-03T21:11:19.859036Z","created_by":"cscheid","updated_at":"2026-05-03T23:29:05.405253Z","closed_at":"2026-05-03T23:29:05.405068Z","close_reason":"Phases 1, 2, 3, and 5 complete. End-to-end verified on bd-5qnj fixtures: big.qmd 16.3 MB pretty -> 62 KB gzipped+deduped (~265x). Unified-artifact promise realized post-merge with bd-45yw. Real-engine supporting_files re-measurement tracked as bd-sr73.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5qnj","depends_on_id":"bd-45yw","type":"related","created_at":"2026-05-03T21:11:19.859036Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} @@ -118,9 +120,11 @@ {"id":"bd-ascs","title":"LSP outline: include cross-referenceable elements (fig, thm, tbl)","description":"Cross-referenceable divs (#fig-, #thm-, #tbl-, etc.) do not appear in the LSP document outline. Headers that semantically belong to a cross-ref target (e.g. '## Line' inside ::: {#thm-line}) currently appear as standalone outline entries instead of being folded into their owning theorem/figure. See claude-notes/plans/2026-04-17-crossref-outline.md for the full plan.","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-04-17T18:42:58.811042Z","created_by":"cscheid","updated_at":"2026-04-17T22:17:28.658125Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-ayj6","title":"[websites phase 9] Hub-client project rendering","description":"Plan: claude-notes/plans/2026-04-23-website-project-epic.md § Phase 9.\n\nDeliverables:\n- New WASM API entry points:\n - build_project_nav(project_dir) -> ProjectNavState\n - render_page_in_project(file_path, project_nav_state) -> HTML\n- Hub-client state: project-scoped nav cache, invalidation on profile-affecting edits (title, frontmatter, draft flag, _quarto.yml sidebar changes).\n- Live preview: editing a page title updates sibling sidebars within one render cycle.\n- API shape must leave room for future Q2 'quarto preview' (local hub-client instance).\n- End-to-end smoke test in a real browser session per CLAUDE.md § End-to-end verification.\n\nBlocked by Phases 2, 3, 5.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-04-23T18:43:28.398865Z","created_by":"cscheid","updated_at":"2026-04-29T00:31:38.160801Z","closed_at":"2026-04-29T00:31:38.160419Z","close_reason":"Phase 9 implemented: Pass2Renderer trait extraction (9.0), ProjectPipeline un-gate for WASM (9.1), WASM Pass-2 renderer + cross-platform flush_site_libs (9.2), render_page_in_project WASM entry point with RenderMode::ActivePage (9.3), hub-client renderToHtml switch + Preview useEffect deps (9.4), hub-smoke fixture + native integration tests (9.5), close-out (9.6). Plan: claude-notes/plans/2026-04-27-websites-phase-9.md. 8072 workspace tests pass; cargo xtask verify (Rust + hub-client WASM build + tests) passes. Browser smoke GIF deferred to a follow-up session.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-ayj6","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-23T18:43:28.398865Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-ayj6","depends_on_id":"bd-h4l6","type":"blocks","created_at":"2026-04-23T18:43:50.534096Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-ayj6","depends_on_id":"bd-mre3","type":"blocks","created_at":"2026-04-23T18:43:48.471854Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-ayj6","depends_on_id":"bd-mw7x","type":"blocks","created_at":"2026-04-23T18:43:49.497210Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-ayj6","depends_on_id":"bd-xee1","type":"blocks","created_at":"2026-04-23T18:44:39.567807Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-b0f2","title":"Fix filenames in pipeline trace astContext","description":"Pipeline trace JSON files always show astContext.files as [{name: \"\"}] instead of the real filename. Root cause: serialize_pandoc_ast in crates/quarto-core/src/stage/trace.rs:417-432 discards doc_ast.ast_context and passes ASTContext::anonymous() to the JSON writer. Secondary issue: EngineExecutionStage rebuilds a single-file context after reconciliation, losing track of both the original .qmd and the intermediate .rmarkdown file. Plan: claude-notes/plans/2026-04-17-trace-anonymous-filenames.md","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-04-17T22:34:43.872061Z","created_by":"cscheid","updated_at":"2026-04-17T23:29:22.136643Z","closed_at":"2026-04-17T23:29:22.136049Z","close_reason":"Fixed trace astContext filenames. Phase 1: trace serializer now threads the real ASTContext through serialize_pipeline_data and on_transform_data. Phase 2: engine_execution builds a two-file merged context (.qmd + .rmarkdown) and pre-remaps the executed AST's FileIds via a new quarto-ast-reconcile::remap_file_ids helper. Plan: claude-notes/plans/2026-04-17-trace-anonymous-filenames.md. 9 new tests, 7444 workspace tests pass, cargo xtask verify passes.","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-b5hf","title":"Phase A.6: Always-visible force-refresh button in q2-preview-spa","description":"Persistent UI affordance that re-runs the WASM render pipeline against current automerge state. The user's escape hatch for cross-doc dep channels the dep graph doesn't yet encode (epic resolution #4). Phase A is engine-less, so this is purely client-side; Phase C extends it to trigger server-side re-execution. See claude-notes/plans/2026-05-13-q2-preview-phase-a.md §A.6.","status":"open","priority":1,"issue_type":"feature","created_at":"2026-05-13T16:53:28.379674Z","created_by":"cscheid","updated_at":"2026-05-13T16:53:28.379674Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-b5hf","depends_on_id":"bd-kw93","type":"parent-child","created_at":"2026-05-13T16:53:28.379674Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-b5jm","title":"L4 — quarto-doctemplate enhancements (pipes, ConfigValue bridge, project resolver)","description":"Implement pipe evaluator (TODO in render_variable + evaluate_partial). MVP pipe set: escape, escape_xml, date_format , first, rest. Add ConfigValue → TemplateValue bridge in quarto-core. Add project-scoped partial resolver (ChainedResolver: MemoryResolver for built-ins + FileSystemResolver rooted at host-page dir). Independent of listings; unblocks L3 and L8. See claude-notes/plans/2026-05-05-listings-epic.md §L4.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-05-05T19:53:14.789389Z","created_by":"cscheid","updated_at":"2026-05-07T13:35:17.667986Z","closed_at":"2026-05-07T13:35:17.667624Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-b5jm","depends_on_id":"bd-61cd","type":"parent-child","created_at":"2026-05-05T19:53:14.789389Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-b9mz","title":"Phase 7 — Post-render (sitemap, favicon, site-url/title)","description":"Phase 7 of bd-0tr6. Per-page transforms (title prefix, favicon link, canonical URL) + post_render writes (favicon copy, sitemap, robots.txt). Sub-plan: claude-notes/plans/2026-04-27-websites-phase-7.md","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-04-27T14:25:29.580640Z","created_by":"cscheid","updated_at":"2026-04-27T15:04:40.508208Z","closed_at":"2026-04-27T15:04:40.507943Z","close_reason":"Implemented on feature/websites: per-page transforms (title prefix, favicon, canonical URL), website_post_render module (favicon copy, sitemap, robots.txt). 7922 workspace tests pass; cargo xtask verify all 9 steps green. Sub-plan: claude-notes/plans/2026-04-27-websites-phase-7.md","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-b9mz","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-27T14:25:29.580640Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-b9za","title":"Extension-dep site_libs/ dedup integration test (Phase 5 deferred)","description":"Phase 5 plan called for tests 19 (extension dep dedup) and 22 (byte-mismatch hard error). The unit-level coverage exists (drain/merge tests in artifact.rs) but an integration test that drives a website with two pages each using the kbd extension and verifies a single _site/site_libs/libs/kbd/kbd.css would close the gap. See claude-notes/plans/2026-04-24-websites-phase-5.md §Tests 19/22.","status":"open","priority":2,"issue_type":"task","created_at":"2026-04-25T01:31:17.461606Z","created_by":"cscheid","updated_at":"2026-04-25T01:31:17.461606Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-b9za","depends_on_id":"bd-u5pr","type":"discovered-from","created_at":"2026-04-25T01:31:17.461606Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-bays","title":"Phase A.7: Playwright smoke test for q2 preview","description":"q2-preview-spa/e2e/basic-preview.spec.ts: spawn quarto preview against a fixture, drive a chromium session, assert (a) initial render contains expected content, (b) editing the .qmd on disk produces a visible change within 2s, (c) force-refresh button works, (d) DOM-stability invariant: a data-stable-id element keeps the same DOM node identity across an edit (===, not just structural equality). See claude-notes/plans/2026-05-13-q2-preview-phase-a.md §A.7.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-13T16:53:28.477270Z","created_by":"cscheid","updated_at":"2026-05-13T16:53:28.477270Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-bays","depends_on_id":"bd-kw93","type":"parent-child","created_at":"2026-05-13T16:53:28.477270Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-bobp","title":"Index-forgiveness for page-source matching in page-nav","description":"Phase 4 uses strict source-path equality (page_source == flat_entry.href) to find the current page in the sidebar flat list. Mirrors the framing of bd-jbml (Phase 3 navbar index-forgiveness): if real content hits about/ vs about/index.qmd drift, allow the same equivalence here.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-04-24T22:48:03.376282Z","created_by":"cscheid","updated_at":"2026-04-24T22:48:03.376282Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-bobp","depends_on_id":"bd-jbml","type":"related","created_at":"2026-04-24T22:48:03.376282Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-bobp","depends_on_id":"bd-nwun","type":"discovered-from","created_at":"2026-04-24T22:48:03.376282Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-bpdz","title":"Reader extension surface for L9 (RSS feed extraction)","description":"L7 ships the listings-only subset of Q1's `readRenderedContents` in `crates/quarto-core/src/project/listing/post_render_upgrade/reader.rs`. The `ReaderOptions` struct has two flags (`remove_links`, `remove_images`) currently no-op in v1's plain-text output.\n\nL9 (RSS feeds, `bd-o90m`) will need:\n- math handling\n- syntax-highlight class maps\n- urls-to-absolute (configurable base URL)\n- anchor stripping\n\nEach is opt-in via a new `ReaderOptions` field. Implement as private functions in the same file gated by their flag. Do not introduce a trait-based plugin architecture (Q1's single-function reader has been stable for years).\n\nFiled at L7 close-out per the L7 plan §\"Filing reminder\" follow-up #2.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-07T19:51:17.456755Z","created_by":"cscheid","updated_at":"2026-05-07T19:51:17.456755Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-bpdz","depends_on_id":"bd-qf7r","type":"discovered-from","created_at":"2026-05-07T19:51:17.456755Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-bqf2","title":"Listings: shared shape walker for parse_listings + extract_content_globs","description":"Today extract_content_globs (project/listing/config.rs) calls parse_listings and discards diagnostics, then plucks Glob entries from each Listing.contents. This re-runs the full listing parse just to read the glob strings.\n\nCleaner approach: factor a shared 'walk_listing_shape' helper that both parse_listings and extract_content_globs can use, so the dep-graph extractor only does the work it needs (walk shapes, collect content globs) and never produces diagnostics.\n\nThis is option 3 from L6 (bd-xbnf) planning. We chose option 1 for L6 because it gives a single source of truth on shape with minimal new code; this issue tracks the refactor for when someone has a reason (perf hot spot, or a second narrow extractor lands).\n\nOrigin: discussed in claude-notes/plans/2026-05-07-listings-L6-dep-graph.md §'Where the globs come from' and during L6 planning.\n\nReference: extract_content_globs in crates/quarto-core/src/project/listing/config.rs has a pointer comment back to this issue.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-07T17:15:52.991267Z","created_by":"cscheid","updated_at":"2026-05-07T17:15:52.991267Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-bqf2","depends_on_id":"bd-xbnf","type":"discovered-from","created_at":"2026-05-07T17:15:52.991267Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} @@ -202,6 +206,7 @@ {"id":"bd-m9rm","title":"Default project render regression and project-level diagnostics","description":"Post-websites-merge: presence of `_quarto.yml` with `project: { type: default }` causes `q2 render` to silently produce zero output, and `q2 render index.qmd` to fail with a misleading 'excluded from render list' error. Root cause: `default_output_dir` returns the project root for default projects, so the discovery walker's output_dir-exclusion check rejects every file. Three goals: (1) fix the discovery regression so default projects walk the tree like websites do; (2) add a clear project-level diagnostic when the render set is empty so we no longer silently no-op; (3) add a project-level diagnostic surface that both the CLI and hub-client can render, since today only file-level diagnostics flow to hub-client. Plan: claude-notes/plans/2026-05-01-default-project-render-diagnostics.md (open design questions inside, awaiting user input before implementation starts).","status":"open","priority":1,"issue_type":"bug","created_at":"2026-05-01T21:40:11.505417Z","created_by":"cscheid","updated_at":"2026-05-01T21:40:11.505417Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-m9rm","depends_on_id":"bd-0tr6","type":"discovered-from","created_at":"2026-05-01T21:40:11.505417Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-mae2","title":"L9 follow-up: custom feed templates (feed.template:)","description":"L9's three feed templates (preamble/item/postamble) are embedded built-ins. L8's listing.template config affects the listing render, not the feed render. A new feed.template: config field would let authors swap in custom XML templates for the feed itself. New feature; not in the L9 scope.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-05-08T17:33:46.431634Z","created_by":"cscheid","updated_at":"2026-05-08T17:33:46.431634Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mae2","depends_on_id":"bd-o90m","type":"discovered-from","created_at":"2026-05-08T17:33:46.431634Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-maz5","title":"Phase 1: Upgrade runtimelib 1.4 → 2.0 in quarto-core","description":"Bump runtimelib and jupyter-protocol from 1.4 to 2.0 in crates/quarto-core/Cargo.toml. Adds a wildcard arm in media_type_to_mime_entry (jupyter-protocol 2.0 makes MediaType #[non_exhaustive]).\n\nStatus: implementation complete in session 2026-05-04, verified with cargo build --workspace, cargo nextest run --workspace (8360 passed), cargo xtask verify --skip-hub-build. End-to-end render still produces 'kernelspec python3 not found' (expected: dirs.rs is unchanged 1.6 → 2.0). Awaiting commit.\n\nPlan: claude-notes/plans/2026-05-04-jupyter-kernelspec-discovery-and-errors.md (Phase 1)","status":"closed","priority":1,"issue_type":"chore","created_at":"2026-05-04T15:49:39.408919Z","created_by":"cscheid","updated_at":"2026-05-04T15:50:40.465475Z","closed_at":"2026-05-04T15:50:40.465325Z","close_reason":"runtimelib 1.4 -> 2.0 upgrade landed in 378d2d54. Workspace builds, all tests pass, end-to-end confirms the bug persists (venv discovery is the next phase, bd-34wy).","source_repo":".","compaction_level":0,"original_size":0,"labels":["chore","jupyter"],"dependencies":[{"issue_id":"bd-maz5","depends_on_id":"bd-fu0l","type":"discovered-from","created_at":"2026-05-04T15:49:39.408919Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-mflk","title":"Phase A.5: End-to-end boot integration test","description":"crates/quarto-preview/tests/boot.rs: spawn 'q2 preview --no-browser --port 0' against a fixture project; assert SPA served, WS handshake succeeds, tempdir created+deleted, launch URL is http://:/#/preview/. See claude-notes/plans/2026-05-13-q2-preview-phase-a.md §A.5.","status":"open","priority":1,"issue_type":"task","created_at":"2026-05-13T16:53:28.279760Z","created_by":"cscheid","updated_at":"2026-05-13T16:53:28.279760Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mflk","depends_on_id":"bd-kw93","type":"parent-child","created_at":"2026-05-13T16:53:28.279760Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-mgoh","title":"Sidebar renders below page content — body class & grid placement wrong","description":"Q2 website rendering places the sidebar at the bottom of the page (below `
      ` and below the prev/next strip) instead of in a left column.\n\nRoot cause (verified against examples/websites/01-minimal rendered with q2 vs q1):\n1. Body class is hardcoded `fullcontent` in crates/quarto-core/src/template.rs:162. The website pipeline never sets the `body-classes` template variable, so we never get `nav-sidebar` or `floating`/`docked`.\n2. resources/scss/bootstrap/_bootstrap-rules.scss keys the grid layout off body classes: `body.floating .page-columns { @include page-columns-float-wide(); }` produces a 150px sidebar column on the left, while `body.fullcontent:not(.floating):not(.docked)` produces no sidebar column.\n3. Without that column, the sidebar wrapper (`#quarto-sidebar-container` placed at `grid-column: body-content-start / body-content-end`, `grid-row: auto`) gets auto-placed in an implicit row after `
      `, so it appears below the page content.\n4. Secondary structural difference: Q1 places `
      ` / `