Skip to content

fix(preview): serve fresh HTML for project pages after .qmd edit#14548

Draft
cderv wants to merge 4 commits into
mainfrom
fix/issue-10392
Draft

fix(preview): serve fresh HTML for project pages after .qmd edit#14548
cderv wants to merge 4 commits into
mainfrom
fix/issue-10392

Conversation

@cderv
Copy link
Copy Markdown
Member

@cderv cderv commented May 26, 2026

When previewing a website or book project, editing a non-index .qmd regenerated the HTML on disk but the preview server kept serving the pre-edit body. Stopping and restarting quarto preview cleared it for the first edit; the next edit reproduced the bug.

Root Cause

watcher.project() returns a long-lived ProjectContext whose fileInformationCache.fullMarkdown caches the expanded markdown per input. The HTTP-handler render in src/project/serve/serve.ts reuses this context, so after a watcher-triggered re-render, projectResolveFullMarkdownForFile returned the pre-edit expanded markdown. The regenerated HTML body therefore reflected the previous content.

Fix

Two layers, motivated separately:

  1. Surgical (src/project/serve/watch.ts): invalidate the project context's fileInformationCache for each changed input inside the submitRender callback. Inside the callback (not before submitRender) so the cache mutation is serialized with any in-flight HTTP-handler render via the existing render queue — invalidateForFile may delete a transient .quarto_ipynb, and running it before enqueue could race a concurrent read.
  2. Defense-in-depth (src/project/project-shared.ts): guard the fullMarkdown cache entry by source file mtime + size. A stale entry is dropped on the next read even if a future caller forgets to invalidate. Size catches the case where filesystems with coarse mtime resolution (e.g. 2-second FAT/SMB) place a rapid edit in the same tick.

Scope

This PR fixes the direct symptom for non-index project pages where the edit is to the .qmd file itself. Related cases raised in the issue thread are not covered here and are tracked separately:

  • Edits to files included by a page (include-shortcode, partial template) — the includer's cache is not invalidated when the includee changes.
  • Cross-reference propagation between sibling pages.

Prior reports on older release lines (#11475 on 1.6, #13755 on 1.8) describe the same root cause; this PR addresses it on main for 1.10.

Test Plan

Automated (tests/unit/project/file-information-cache.test.ts): mtime guard, size guard, per-file invalidation.

Manual spec: tests/docs/manual/preview/10392-project-preview-stale-cache-after-edit.md.

  • Edit a non-index .qmd in a website project — served HTML reflects the edit (T1)
  • Repeat without restarting preview — second edit also reflects (T2)
  • Edit index.qmd — no regression on the index path (T3)
  • Concurrent saves on a Jupyter input — no file-not-found errors in preview console (T7, race regression)
  • Single-file preview Jupyter regression — .quarto_ipynb accumulation tests pass

Backport

Not included here. A v1.9.x backport will be considered alongside the next stable patch.

Fixes #10392

cderv added 4 commits May 26, 2026 17:10
In project preview mode, the persistent ProjectContext returned by
watcher.project() owns a long-lived fileInformationCache populated at
preview startup. When the watcher fires on a source edit, two render
paths follow: the watcher itself dispatches to a render call that
builds an ephemeral context (no stale cache), but the subsequent
HTTP-handler render in serve.ts reuses the persistent context and
reads the pre-edit expanded markdown back out of cache.fullMarkdown.
The regenerated HTML mtime advances while the body content stays at
the pre-edit revision (#10392).

Invalidate cache entries for each changed input before the watcher's
render dispatch, mirroring the renderForPreview pattern in
src/command/preview/preview.ts. A companion commit adds a source-mtime
and size fingerprint inside projectResolveFullMarkdownForFile so the
cache contract self-validates even if a future caller forgets to
invalidate; this surgical fix is the single-commit cherry-pick target
for the v1.9 backport.

Closes #10392.
The persistent ProjectContext used by website/book preview kept a
fileInformationCache.fullMarkdown entry populated at startup with no
freshness fingerprint, so subsequent renders fed Pandoc the pre-edit
expanded markdown for the HTTP-handler renderProject() call site.

Adds sourceMtime + sourceSize fields on FileInformation and re-reads
when either differs from the cached value. Pairs with the watcher-side
invalidation in the preceding commit; this layer closes the contract
gap for any future caller that forgets to invalidate. Size is included
alongside mtime to catch the edge case where an edit lands within a
single mtime tick on a coarse-resolution filesystem but changes the
byte count.

Relates to #10392.
…#10392)

Manual T1/T2/T3 (P1) plus T4-T6 (P2/P3) covering the project preview
stale-render reproduction. Reproduces deterministically against the
existing website fixture at tests/docs/manual/preview/project-preview/.

Originally drafted on the debug/preview-cache-logging investigation
branch; landing here so the spec accompanies the fix.

Relates to #10392.
@posit-snyk-bot
Copy link
Copy Markdown
Collaborator

posit-snyk-bot commented May 26, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

quarto preview does not update all pages when project-wide contents change

2 participants