Skip to content

Preview does not pick up an added or removed _brand.yml until restarted #14593

@cderv

Description

@cderv

A quarto preview session resolves _brand.yml once and caches the result for the lifetime of the process. Adding, removing, or changing a sibling _brand.yml while preview is running has no effect on the output until preview is stopped and started again.

Reproduction

Start from a single document with no _brand.yml next to it:

---
title: "Report"
format: typst
---

Voir la [documentation](https://example.com).
quarto preview report.qmd --to typst --no-watch-inputs --no-browser

Now add a _brand.yml in the same directory:

color:
  palette:
    imperial-red: "#BC1E22"
  primary: imperial-red

Trigger a re-render (request a render, or edit and save the document). The link keeps its default color. Stop preview and render again and the brand is applied — the link turns red. The reverse case behaves the same way: start with _brand.yml present, remove it, and the brand stays applied until preview is restarted.

This is also what shows up from the RStudio IDE. The Render button runs quarto preview ... --no-watch-inputs and routes the following renders through the same long-lived preview server, so creating a _brand.yml and clicking Render again produces no visible change.

Cause

The preview server keeps a single ProjectContext for the whole session. projectResolveBrand populates project.brandCache on the first resolve and returns it on every subsequent call without re-scanning for _brand.yml:

if (fileName === undefined) {
if (project.brandCache) {
return project.brandCache.brand;
}
project.brandCache = {};
let fileNames = [
"_brand.yml",
"_brand.yaml",
"_brand/_brand.yml",
"_brand/_brand.yaml",
].map((file) => join(project.dir, file));
const brand = (project?.config?.brand ??
project?.config?.project.brand) as
| boolean
| string
| {
light?: string;
dark?: string;
};
if (brand === false) {
project.brandCache.brand = undefined;
return project.brandCache.brand;
}
if (
typeof brand === "object" && brand &&
("light" in brand || "dark" in brand)
) {
project.brandCache.brand = {
light: brand.light
? await loadSingleBrand(resolveBrandPath(brand.light, project.dir))
: undefined,
dark: brand.dark
? await loadSingleBrand(resolveBrandPath(brand.dark, project.dir))
: undefined,
enablesDarkMode: !!brand.dark,
};
return project.brandCache.brand;
}
if (typeof brand === "string") {
fileNames = [join(project.dir, brand)];
}
for (const brandPath of fileNames) {
if (!existsSync(brandPath)) {
continue;
}
project.brandCache.brand = await loadUnifiedBrand(brandPath);
}
return project.brandCache.brand;

Re-renders invalidate the per-file fileInformationCache but never brandCache:

if (project?.fileInformationCache) {
project.fileInformationCache.invalidateForFile(file);
}

A separate quarto render process resolves brand correctly because it builds a fresh ProjectContext; only the long-lived preview process is affected. Single-file and project (_quarto.yml) previews are both affected since they share the same cache field, and the behavior is format-independent (projectResolveBrand takes no format).

Suggestion

We could give brandCache a fingerprint over the candidate brand paths (_brand.yml, _brand.yaml, _brand/_brand.yml, _brand/_brand.yaml) covering existence and modification time, and re-resolve when it changes — similar to the mtime guard already used for the full-markdown cache.

Metadata

Metadata

Assignees

Labels

brand`_brand.yml`bugSomething isn't workingpreviewissues related to the `preview` command

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions