Follow-up to #13819 / #13824 — the filter that strips bundled engines from output metadata only matches paths in the source tree. In an installed Quarto distribution, bundled engines live under share/extension-subtrees/, not resources/extension-subtrees/, so the filter is a no-op and the bundled julia-engine.js absolute path leaks into the rendered YAML metadata.
Reproduce
Minimal document:
---
title: Basic doc
format: markdown
---
It should be markdown engine.
With Quarto installed via scoop on Windows (any package-manager install with the standard share/ layout reproduces it):
Output index.md contains a leaked engines: field:
---
engines:
- path: "C:\\Users\\chris\\scoop\\apps\\quarto-prerelease\\current\\share\\extension-subtrees\\julia-engine_extensions\\julia-engine\\julia-engine.js"
title: Basic doc
---
(When the output format is markdown, the markdown writer escapes each \ as a `{=tex}` raw block, so the leaked path also looks visually mangled in the file. Cosmetic side effect of the same leak.)
The same engines block also appears in the metadata logged for other formats (html, etc.), same as the original #13819 report.
Root cause
Two filter sites both check the substring resources/extension-subtrees/:
|
// Filter out bundled engines from the engines array |
|
if (Array.isArray(metadata.engines)) { |
|
const filteredEngines = metadata.engines.filter((engine) => { |
|
const enginePath = typeof engine === "string" ? engine : engine.path; |
|
// Keep user engines, filter out bundled ones |
|
return !enginePath?.replace(/\\/g, "/").includes( |
|
"resources/extension-subtrees/", |
|
); |
|
}); |
|
|
|
// Remove the engines key entirely if empty, otherwise assign filtered array |
|
if (filteredEngines.length === 0) { |
|
delete metadata.engines; |
|
} else { |
|
metadata.engines = filteredEngines; |
|
} |
|
} |
|
// Filter out bundled engines from metadata passed to Pandoc |
|
if (Array.isArray(pandocPassedMetadata.engines)) { |
|
const filteredEngines = pandocPassedMetadata.engines.filter((engine) => { |
|
const enginePath = typeof engine === "string" ? engine : engine.path; |
|
if (!enginePath) return true; |
|
return !enginePath.replace(/\\/g, "/").includes( |
|
"resources/extension-subtrees/", |
|
); |
|
}); |
|
|
|
if (filteredEngines.length === 0) { |
|
delete pandocPassedMetadata.engines; |
|
} else { |
|
pandocPassedMetadata.engines = filteredEngines; |
|
} |
|
} |
At runtime, builtinSubtreeExtensions() resolves engine paths through resourcePath("extension-subtrees"), which joins sharePath() with extension-subtrees:
|
// Path for built-in extensions |
|
function builtinExtensions() { |
|
return resourcePath("extensions"); |
|
} |
|
|
|
// Path for built-in subtree extensions |
|
export function builtinSubtreeExtensions() { |
|
return resourcePath("extension-subtrees"); |
|
} |
|
export function resourcePath(resource?: string): string { |
|
const sharePath = quartoConfig.sharePath(); |
|
if (resource) { |
|
return join(sharePath, resource); |
|
} else { |
|
return sharePath; |
|
} |
|
} |
So in an installed distro, bundled engine paths look like <install>/share/extension-subtrees/... and never contain the substring resources/extension-subtrees/. Filter is a no-op.
This is a regression of #13819 — PR #13824 fixed the leak when running from the source tree but anchored the matcher to the dev-tree path. Commit 7c0e332 ("cross-plat") later normalized backslashes but kept the same substring.
The existing smoke test https://github.com/quarto-dev/quarto-cli/blob/36e2232273cb9de9af82f2bbb946a0a36808b425/tests/docs/smoke-all/2025/12/22/markdown-engine-no-extension-subtrees.qmd doesn't catch this because CI runs from the source tree, where the dev-tree path does match the filter.
Suggested fix
We could match the unique directory name extension-subtrees/, or better, compare engine paths against builtinSubtreeExtensions() directly so the check is anchored to the actual bundled-extension root regardless of source vs. installed layout.
Related: #14527, #14528 (other facets of internal extensions being treated like user extensions).
Follow-up to #13819 / #13824 — the filter that strips bundled engines from output metadata only matches paths in the source tree. In an installed Quarto distribution, bundled engines live under
share/extension-subtrees/, notresources/extension-subtrees/, so the filter is a no-op and the bundledjulia-engine.jsabsolute path leaks into the rendered YAML metadata.Reproduce
Minimal document:
With Quarto installed via scoop on Windows (any package-manager install with the standard
share/layout reproduces it):Output
index.mdcontains a leakedengines:field:(When the output format is markdown, the markdown writer escapes each
\as a`{=tex}`raw block, so the leaked path also looks visually mangled in the file. Cosmetic side effect of the same leak.)The same engines block also appears in the metadata logged for other formats (html, etc.), same as the original #13819 report.
Root cause
Two filter sites both check the substring
resources/extension-subtrees/:quarto-cli/src/command/render/pandoc.ts
Lines 442 to 458 in 36e2232
quarto-cli/src/command/render/pandoc.ts
Lines 1326 to 1341 in 36e2232
At runtime,
builtinSubtreeExtensions()resolves engine paths throughresourcePath("extension-subtrees"), which joinssharePath()withextension-subtrees:quarto-cli/src/extension/extension.ts
Lines 703 to 711 in 36e2232
quarto-cli/src/core/resources.ts
Lines 20 to 27 in 36e2232
So in an installed distro, bundled engine paths look like
<install>/share/extension-subtrees/...and never contain the substringresources/extension-subtrees/. Filter is a no-op.This is a regression of #13819 — PR #13824 fixed the leak when running from the source tree but anchored the matcher to the dev-tree path. Commit 7c0e332 ("cross-plat") later normalized backslashes but kept the same substring.
The existing smoke test https://github.com/quarto-dev/quarto-cli/blob/36e2232273cb9de9af82f2bbb946a0a36808b425/tests/docs/smoke-all/2025/12/22/markdown-engine-no-extension-subtrees.qmd doesn't catch this because CI runs from the source tree, where the dev-tree path does match the filter.
Suggested fix
We could match the unique directory name
extension-subtrees/, or better, compare engine paths againstbuiltinSubtreeExtensions()directly so the check is anchored to the actual bundled-extension root regardless of source vs. installed layout.Related: #14527, #14528 (other facets of internal extensions being treated like user extensions).