Skip to content

fix: detect Quarto engines in fallback path and normalize backfill#4098

Closed
zackverham wants to merge 3 commits into
mainfrom
fix/quarto-engines-detection-3993
Closed

fix: detect Quarto engines in fallback path and normalize backfill#4098
zackverham wants to merge 3 commits into
mainfrom
fix/quarto-engines-detection-3993

Conversation

@zackverham

Copy link
Copy Markdown
Collaborator

Summary

Fixes #3993

  • Fallback engine detection for .qmd files: When quarto inspect is unavailable (quarto not installed or not in PATH), the fallback path now reads .qmd file content and scans for {r} and {python} code chunks, setting the appropriate engines (knitr/jupyter) and language configs. Previously, .qmd files in the fallback path got no engines at all, while .ipynb and .Rmd already had correct handling.
  • Normalize engine backfill: normalizeConfig() now ensures quarto.engines includes knitr/jupyter when it independently discovers R/Python needs (e.g., via renv.lock or rpy2). This is a defensive fix — if the detector missed engines for any reason, normalize catches it before the config is written to TOML.

Without these fixes, Connect receives engines: [] in the manifest, which causes it to skip renv restore entirely, leading to missing packages at render time.

Test plan

  • New unit tests for fallback R, Python, and mixed engine detection in quarto.test.ts
  • New unit tests for engine backfill (knitr via renv.lock, jupyter, dedup, no-op without quarto) in normalize.test.ts
  • All 1676 existing unit tests pass
  • Lint passes
  • Manual test: publish a .qmd with {r} chunks and renv.lock from Positron, verify manifest has engines: ["knitr"]

🤖 Generated with Claude Code

…3993)

When quarto inspect is unavailable, the fallback path for .qmd files
was not reading file content to detect R/Python code chunks, resulting
in an empty engines array in the manifest. Connect uses this field to
determine whether to restore the renv environment, so an empty engines
array caused renv restore to be skipped entirely.

Additionally, normalizeConfig now backfills knitr/jupyter engines when
it independently discovers R/Python needs (e.g., via renv.lock), ensuring
engines stay consistent even if the detector missed them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zackverham zackverham requested a review from a team as a code owner April 29, 2026 17:40
@posit-connect-projects

posit-connect-projects Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @zackverham's task in 2m 14s —— View job


Code Review Complete

  • Read and analyze changed files
  • Review quarto.ts fallback engine detection changes
  • Review normalize.ts engine backfill changes
  • Review test coverage
  • Post review feedback

Summary

This PR fixes a real issue where Connect receives engines: [] in the manifest when quarto inspect is unavailable, causing renv restore to be skipped. The changes are well-structured and the fix is in the right place.

Review

Code Quality: Good

quarto.ts (lines 510-539) - Fallback engine detection for .qmd files:

  • Correctly reads file content and uses existing detectMarkdownLanguagesInContent() helper
  • Properly avoids duplicate engines with !engines.includes() checks
  • Engines are sorted before assignment (line 537)
  • Follows the existing patterns for .ipynb and .rmd handling

normalize.ts (lines 129-148) - Engine backfill:

  • Defensive fix that catches missed engines before config is written
  • Creates a copy of the engines array before modification (immutable approach)
  • Properly checks for existing engines to avoid duplicates
  • Only modifies quartoConfig when changes are actually made

Test Coverage: Comprehensive

The new tests cover:

  • quarto.test.ts: R-only, Python-only, and mixed R+Python detection in fallback (lines 716-768)
  • normalize.test.ts: Engine backfill via renv.lock, Python discovery, deduplication, and no-op cases (lines 326-384)

Minor Consideration

In genNonInspectConfig() at line 511:

const qmdFiles = await globDir(baseDir, "*.qmd");

This doesn't use { nocase: true } like findEntrypoints() does (line 441). On case-sensitive filesystems (Linux), files named Report.QMD wouldn't be found in fallback mode. This is a very minor edge case and may be intentional, but worth noting for consistency.

Verdict

Approve - The fix is correct, well-tested, and addresses the root cause. The code follows existing patterns and the test coverage is solid.


@zackverham

Copy link
Copy Markdown
Collaborator Author

@claude can you fix the minor consideration noted in the PR review comment?

@posit-connect-projects

posit-connect-projects Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @zackverham's task in 1m 0s —— View job


Fixed case-insensitive glob for .qmd files

  • Read the relevant code in quarto.ts
  • Add { nocase: true } to the .qmd glob in genNonInspectConfig()
  • Commit and push the fix

Added { nocase: true } to the globDir(baseDir, "*.qmd") call at line 511 of quarto.ts, matching the pattern used in findEntrypoints() at line 440. This ensures files like Report.QMD are found on case-sensitive filesystems (Linux).

Add { nocase: true } to the globDir call for .qmd files in
genNonInspectConfig() for consistency with findEntrypoints(). This
ensures files like Report.QMD are found on case-sensitive filesystems.

Co-authored-by: zackverham <zackverham@users.noreply.github.com>
@zackverham

Copy link
Copy Markdown
Collaborator Author

@claude can you add a changelog blurb for this PR

@posit-connect-projects

posit-connect-projects Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @zackverham's task in 48s —— View job


Adding Changelog Entry


Added a changelog entry under [Unreleased] > Fixed:

Fixed Quarto .qmd files being deployed with empty engines when quarto inspect is unavailable, causing Connect to skip renv restore and fail at render time due to missing packages. The fallback path now detects knitr and jupyter engines from code chunks, and normalizeConfig() backfills missing engines when R or Python dependencies are present. (#3993)
·

Co-authored-by: zackverham <zackverham@users.noreply.github.com>
@zackverham zackverham self-assigned this Apr 29, 2026
@zackverham

Copy link
Copy Markdown
Collaborator Author

Smoke Test Plan

Setup

  • Use sample .qmd content from test/sample-content/ or create minimal .qmd files as described below.
  • Tests should be run in Positron or VSCode with the Publisher extension installed from the PR build.

Scenario 1: .qmd with R chunks, quarto NOT installed

  1. Open a project with a .qmd file containing {r} code chunks and an renv.lock file.
  2. Ensure quarto is not on $PATH (e.g., temporarily rename/remove it).
  3. Trigger a new deployment via Publisher.
  4. Verify: The generated configuration has engines: ["knitr"] and an r section.
  5. Verify: Deploy to Connect succeeds — Connect restores renv packages and renders correctly.

Scenario 2: .qmd with Python chunks, quarto NOT installed

  1. Open a project with a .qmd file containing {python} code chunks.
  2. Ensure quarto is not on $PATH.
  3. Trigger a new deployment via Publisher.
  4. Verify: The generated configuration has engines: ["jupyter"] and a python section.

Scenario 3: .qmd with both R and Python chunks, quarto NOT installed

  1. Open a project with a .qmd file containing both {r} and {python} code chunks.
  2. Ensure quarto is not on $PATH.
  3. Trigger a new deployment.
  4. Verify: Configuration has engines: ["jupyter", "knitr"] (sorted), plus both r and python sections.

Scenario 4: .qmd with R chunks, quarto IS installed

  1. Open a project with a .qmd file containing {r} code chunks.
  2. Ensure quarto is available on $PATH.
  3. Trigger a new deployment.
  4. Verify: quarto inspect path is used (check debug logs). Engines are set correctly. No regression from the fallback changes.

Scenario 5: Normalize backfill — renv.lock present but engines missing

  1. Open a Quarto project with an renv.lock but where the initial detection somehow produces an empty engines array (can simulate by inspecting the generated TOML before deploy).
  2. Verify: After normalization, the config includes engines: ["knitr"] — the backfill catches the gap.

Scenario 6: Non-Quarto content unaffected

  1. Open an R Shiny app (app.R) or a plain Python project.
  2. Deploy via Publisher.
  3. Verify: No quarto section appears in the configuration. No regressions.

@zackverham

zackverham commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator Author

Using these to validate locally - just commenting them here for reproducibility.

Smoke Test Files

Minimal .qmd files for testing the scenarios in the smoke test plan above.

Scenario 1: R only (report.qmd + renv.lock)

report.qmd

---
title: "R Only Report"
format: html
---

```{r}
library(ggplot2)

ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point()

**`renv.lock`**
```json
{
  "R": {
    "Version": "4.4.1",
    "Repositories": [
      {
        "Name": "CRAN",
        "URL": "https://cloud.r-project.org"
      }
    ]
  },
  "Packages": {
    "ggplot2": {
      "Package": "ggplot2",
      "Version": "3.5.1",
      "Source": "Repository",
      "Repository": "CRAN"
    }
  }
}

Scenario 2: Python only (analysis.qmd)

analysis.qmd

---
title: "Python Only Analysis"
format: html
---

```{python}
import pandas as pd

df = pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
df.head()

---

### Scenario 3: Mixed R + Python (`mixed.qmd` + `renv.lock`)

**`mixed.qmd`**
```qmd
---
title: "Mixed R and Python"
format: html
---

```{r}
library(ggplot2)

ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point()
import pandas as pd

df = pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
df.head()

Use the same `renv.lock` from Scenario 1.

@zackverham

Copy link
Copy Markdown
Collaborator Author

Closing this in favor of #4128

@zackverham zackverham closed this May 15, 2026
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 document with sourced R file generates empty engines field in manifest, causing Connect to skip renv restore

2 participants