From 4e5d4b4d576b8ec8de466c5cce1671bb535819ce Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 3 Jul 2025 17:38:34 +0200 Subject: [PATCH 01/24] feat: add project_path() function for Quarto-aware path construction - Add project_path() function with intelligent project root detection - Support Quarto environment variables (QUARTO_PROJECT_ROOT, QUARTO_PROJECT_DIR) - Fallback to extended project detection (_quarto.yml, .vscode, DESCRIPTION, .Rproj) - Add is_running_quarto_project() to detect Quarto execution context - Add is_quarto_project() to detect Quarto project structure - Mark as experimental with lifecycle badge --- DESCRIPTION | 1 + NAMESPACE | 4 + R/quarto-package.R | 1 + R/utils-projects.R | 210 +++++++++++++++++++++++++ man/figures/lifecycle-deprecated.svg | 21 +++ man/figures/lifecycle-experimental.svg | 21 +++ man/figures/lifecycle-stable.svg | 29 ++++ man/figures/lifecycle-superseded.svg | 21 +++ man/is_quarto_project.Rd | 41 +++++ man/is_running_quarto_project.Rd | 39 +++++ man/project_path.Rd | 90 +++++++++++ 11 files changed, 478 insertions(+) create mode 100644 R/utils-projects.R create mode 100644 man/figures/lifecycle-deprecated.svg create mode 100644 man/figures/lifecycle-experimental.svg create mode 100644 man/figures/lifecycle-stable.svg create mode 100644 man/figures/lifecycle-superseded.svg create mode 100644 man/is_quarto_project.Rd create mode 100644 man/is_running_quarto_project.Rd create mode 100644 man/project_path.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 3fd70fb4..aef7cefd 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,6 +23,7 @@ Imports: htmltools, jsonlite, later, + lifecycle, processx, rlang, rmarkdown, diff --git a/NAMESPACE b/NAMESPACE index c26973a7..68a74556 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,8 +2,11 @@ export(add_spin_preamble) export(check_newer_version) +export(is_quarto_project) +export(is_running_quarto_project) export(is_using_quarto) export(new_blog_post) +export(project_path) export(qmd_to_r_script) export(quarto_add_extension) export(quarto_available) @@ -47,6 +50,7 @@ importFrom(htmltools,div) importFrom(htmltools,span) importFrom(jsonlite,fromJSON) importFrom(later,later) +importFrom(lifecycle,deprecated) importFrom(processx,process) importFrom(processx,run) importFrom(rlang,caller_env) diff --git a/R/quarto-package.R b/R/quarto-package.R index 660855c6..2b33d307 100644 --- a/R/quarto-package.R +++ b/R/quarto-package.R @@ -6,6 +6,7 @@ #' @importFrom cli cli_inform #' @importFrom htmltools div #' @importFrom htmltools span +#' @importFrom lifecycle deprecated #' @importFrom rlang caller_env #' @importFrom tools vignetteEngine #' @importFrom xfun base64_encode diff --git a/R/utils-projects.R b/R/utils-projects.R new file mode 100644 index 00000000..a4d97fe8 --- /dev/null +++ b/R/utils-projects.R @@ -0,0 +1,210 @@ +#' Get path relative to project root (Quarto-aware) +#' +#' @description +#' `r lifecycle::badge("experimental")` +#' +#' This function constructs file paths relative to the project root when +#' running in a Quarto context (using `QUARTO_PROJECT_ROOT` or `QUARTO_PROJECT_DIR` +#' environment variables), or falls back to intelligent project root detection +#' when not in a Quarto context. +#' +#' It is experimental and subject to change in future releases. The automatic +#' project root detection may not work reliably in all contexts, especially when +#' projects have complex directory structures or when running in non-standard +#' environments. For a more explicit and potentially more robust approach, +#' consider using [here::i_am()] to declare your project structure, +#' followed by [here::here()] for path construction. See examples for comparison. +#' +#' @details +#' The function uses the following fallback hierarchy to determine the project root: +#' +#' - Quarto environment variables set during Quarto commands (e.g., `quarto render`): +#' - `QUARTO_PROJECT_ROOT` environment variable (set by Quarto commands) +#' - `QUARTO_PROJECT_DIR` environment variable (alternative Quarto variable) +#' +#' - Fallback to intelligent project root detection using [xfun::proj_root()] for interactive sessions: +#' - `_quarto.yml` or `_quarto.yaml` (Quarto project files) +#' - `.vscode` directory (VS Code/Positron workspace) +#' - `DESCRIPTION` file with `Package:` field (R package or Project) +#' - `.Rproj` files with `Version:` field (RStudio projects) +#' +#' Last fallback is the current working directory if no project root can be determined. +#' A warning is issued to alert users that behavior may differ between interactive use and Quarto rendering, +#' as in this case the computed path may be wrong. +#' +#' @param ... Character vectors of path components to be joined +#' @param root Project root directory. If `NULL` (default), automatic detection +#' is used following the hierarchy described above +#' @return A character vector of the normalized file path relative to the project root +#' +#' @examples +#' \dontrun{ +#' # Reference a data file from project root +#' data_path <- quarto::project_path("data", "my_data.csv") +#' +#' # Reference a script +#' script_path <- quarto::project_path("R", "analysis.R") +#' +#' # Reference nested directories +#' output_path <- quarto::project_path("outputs", "figures", "plot.png") +#' +#' # Explicitly specify root (overrides automatic detection) +#' custom_path <- quarto::project_path("data", "file.csv", root = "/path/to/project") +#' +#' # Alternative approach using here::i_am() (potentially more robust) +#' # This approach requires you to declare where you are in the project: +#' if (requireNamespace("here", quietly = TRUE)) { +#' # Declare that this document is in the project root or subdirectory +#' here::i_am("analysis.qmd") # If in project root +#' # here::i_am("reports/analysis.qmd") # If in subdirectory +#' +#' # Now here::here() will work reliably from the project root +#' data_path_alt <- here::here("data", "my_data.csv") +#' script_path_alt <- here::here("R", "analysis.R") +#' output_path_alt <- here::here("outputs", "figures", "plot.png") +#' } +#' } +#' +#' @seealso +#' * [here::here()] for a similar function that works with R projects +#' * [is_running_quarto_project()] to check if quarto is running with a project context +#' * [xfun::from_root()] for the underlying path construction +#' * [xfun::proj_root()] for project root detection logic +#' +#' @export +project_path <- function(..., root = NULL) { + if (is.null(root)) { + # Try Quarto project environment variables first + quarto_root <- Sys.getenv( + "QUARTO_PROJECT_ROOT", + Sys.getenv("QUARTO_PROJECT_DIR") + ) + + if (nzchar(quarto_root)) { + root <- quarto_root + } else { + # Try to find project root using xfun::proj_root() with extended rules + tryCatch( + { + # Create extended rules that include Quarto and VS Code project files + extended_rules <- rbind( + # this should be the same as Quarto environment variables + # which are only set when running Quarto commands + c("_quarto.yml", ""), # Quarto project config + c("_quarto.yaml", ""), # Alternative Quarto config + # This is to provide some better fallback than just the working directory + c(".vscode", ""), # VS Code/Positron workspace + xfun::root_rules # Default rules (DESCRIPTION, .Rproj) + ) + + proj_root <- xfun::proj_root(rules = extended_rules) + root <- if (!is.null(proj_root)) { + proj_root + } else { + cli::cli_warn( + "Failed to determine project root using {.fun xfun::proj_root}. Using current working directory.", + ">" = "This may lead to different behavior interactively vs running Quarto commands." + ) + getwd() + } + }, + error = function(e) { + # Fall back to working directory if proj_root() fails + cli::cli_warn(c( + "Failed to determine project root: {e$message}. Using current working directory as a fallback.", + ">" = "This may lead to different behavior interactively vs running Quarto commands." + )) + root <- getwd() + } + ) + } + } + + # Use xfun::from_root for better path handling + path <- tryCatch( + xfun::from_root(..., root = root, error = FALSE), + error = function(e) file.path(root, ...) + ) + path +} + +#' Check if running within a Quarto project context +#' +#' @description +#' This function checks if the current R session is running within a Quarto +#' project context by detecting Quarto project environment variables. +#' +#' @details +#' Quarto sets `QUARTO_PROJECT_ROOT` and `QUARTO_PROJECT_DIR` environment +#' variables when executing commands within a Quarto project context (e.g., +#' `quarto render`, `quarto preview`). This function detects their presence. +#' +#' Note that this function will return `FALSE` when running code interactively +#' in an IDE (even within a Quarto project directory), as these specific +#' environment variables are only set during Quarto command execution. +#' +#' @return Logical indicating if Quarto project environment variables are set +#' +#' @seealso +#' * [is_quarto_project()] for checking Quarto project structure +#' * [project_path()] for constructing paths relative to the project root +#' @examples +#' \dontrun{ +#' # This will be TRUE during `quarto render` in a project +#' is_running_quarto_project() +#' +#' # This will be FALSE when not running during `quarto_render` (e.g. interactively) +#' is_running_quarto_project() +#' } +#' @export +is_running_quarto_project <- function() { + nzchar(Sys.getenv("QUARTO_PROJECT_ROOT")) || + nzchar(Sys.getenv("QUARTO_PROJECT_DIR")) +} + +#' Check if working within a Quarto project structure +#' +#' @description +#' This function checks if the current working directory is within a Quarto +#' project by looking for Quarto project files (`_quarto.yml` or `_quarto.yaml`). +#' Unlike [is_running_quarto_project()], this works both during rendering and +#' interactive sessions. +#' +#' @param path Character. Path to check for Quarto project files. Defaults to +#' current working directory. +#' +#' @return Logical indicating if a Quarto project structure is detected +#' +#' @examplesIf quarto_available() +#' dir <- tempfile() +#' dir.create(dir) +#' is_quarto_project(dir) +#' quarto_create_project(dir) +#' is_quarto_project(dir) +#' +#' xfun::in_dir(dir, +#' # Check if current directory is in a Quarto project +#' is_quarto_project() +#' ) +#' # clean up +#' unlink(dir, recursive = TRUE) +#' +#' +#' @seealso [is_running_quarto_project()] for detecting active Quarto rendering +#' @export +is_quarto_project <- function(path = ".") { + tryCatch( + { + quarto_rules <- rbind( + c("_quarto.yml", ""), + c("_quarto.yaml", "") + ) + + proj_root <- xfun::proj_root(path = path, rules = quarto_rules) + !is.null(proj_root) + }, + error = function(e) { + FALSE + } + ) +} diff --git a/man/figures/lifecycle-deprecated.svg b/man/figures/lifecycle-deprecated.svg new file mode 100644 index 00000000..b61c57c3 --- /dev/null +++ b/man/figures/lifecycle-deprecated.svg @@ -0,0 +1,21 @@ + + lifecycle: deprecated + + + + + + + + + + + + + + + lifecycle + + deprecated + + diff --git a/man/figures/lifecycle-experimental.svg b/man/figures/lifecycle-experimental.svg new file mode 100644 index 00000000..5d88fc2c --- /dev/null +++ b/man/figures/lifecycle-experimental.svg @@ -0,0 +1,21 @@ + + lifecycle: experimental + + + + + + + + + + + + + + + lifecycle + + experimental + + diff --git a/man/figures/lifecycle-stable.svg b/man/figures/lifecycle-stable.svg new file mode 100644 index 00000000..9bf21e76 --- /dev/null +++ b/man/figures/lifecycle-stable.svg @@ -0,0 +1,29 @@ + + lifecycle: stable + + + + + + + + + + + + + + + + lifecycle + + + + stable + + + diff --git a/man/figures/lifecycle-superseded.svg b/man/figures/lifecycle-superseded.svg new file mode 100644 index 00000000..db8d757f --- /dev/null +++ b/man/figures/lifecycle-superseded.svg @@ -0,0 +1,21 @@ + + lifecycle: superseded + + + + + + + + + + + + + + + lifecycle + + superseded + + diff --git a/man/is_quarto_project.Rd b/man/is_quarto_project.Rd new file mode 100644 index 00000000..47baec4a --- /dev/null +++ b/man/is_quarto_project.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-projects.R +\name{is_quarto_project} +\alias{is_quarto_project} +\title{Check if working within a Quarto project structure} +\usage{ +is_quarto_project(path = ".") +} +\arguments{ +\item{path}{Character. Path to check for Quarto project files. Defaults to +current working directory.} +} +\value{ +Logical indicating if a Quarto project structure is detected +} +\description{ +This function checks if the current working directory is within a Quarto +project by looking for Quarto project files (\verb{_quarto.yml} or \verb{_quarto.yaml}). +Unlike \code{\link[=is_running_quarto_project]{is_running_quarto_project()}}, this works both during rendering and +interactive sessions. +} +\examples{ +\dontshow{if (quarto_available()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +dir <- tempfile() +dir.create(dir) +is_quarto_project(dir) +quarto_create_project(dir) +is_quarto_project(dir) + +xfun::in_dir(dir, + # Check if current directory is in a Quarto project + is_quarto_project() +) +# clean up +unlink(dir, recursive = TRUE) + +\dontshow{\}) # examplesIf} +} +\seealso{ +\code{\link[=is_running_quarto_project]{is_running_quarto_project()}} for detecting active Quarto rendering +} diff --git a/man/is_running_quarto_project.Rd b/man/is_running_quarto_project.Rd new file mode 100644 index 00000000..359a031a --- /dev/null +++ b/man/is_running_quarto_project.Rd @@ -0,0 +1,39 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-projects.R +\name{is_running_quarto_project} +\alias{is_running_quarto_project} +\title{Check if running within a Quarto project context} +\usage{ +is_running_quarto_project() +} +\value{ +Logical indicating if Quarto project environment variables are set +} +\description{ +This function checks if the current R session is running within a Quarto +project context by detecting Quarto project environment variables. +} +\details{ +Quarto sets \code{QUARTO_PROJECT_ROOT} and \code{QUARTO_PROJECT_DIR} environment +variables when executing commands within a Quarto project context (e.g., +\verb{quarto render}, \verb{quarto preview}). This function detects their presence. + +Note that this function will return \code{FALSE} when running code interactively +in an IDE (even within a Quarto project directory), as these specific +environment variables are only set during Quarto command execution. +} +\examples{ +\dontrun{ +# This will be TRUE during `quarto render` in a project +is_running_quarto_project() + +# This will be FALSE when running interactively +is_running_quarto_project() +} +} +\seealso{ +\itemize{ +\item \code{\link[=is_quarto_project]{is_quarto_project()}} for checking Quarto project structure +\item \code{\link[=project_path]{project_path()}} for constructing paths relative to the project root +} +} diff --git a/man/project_path.Rd b/man/project_path.Rd new file mode 100644 index 00000000..4ed9d255 --- /dev/null +++ b/man/project_path.Rd @@ -0,0 +1,90 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-projects.R +\name{project_path} +\alias{project_path} +\title{Get path relative to project root (Quarto-aware)} +\usage{ +project_path(..., root = NULL) +} +\arguments{ +\item{...}{Character vectors of path components to be joined} + +\item{root}{Project root directory. If \code{NULL} (default), automatic detection +is used following the hierarchy described above} +} +\value{ +A character vector of the normalized file path relative to the project root +} +\description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} + +This function constructs file paths relative to the project root when +running in a Quarto context (using \code{QUARTO_PROJECT_ROOT} or \code{QUARTO_PROJECT_DIR} +environment variables), or falls back to intelligent project root detection +when not in a Quarto context. + +It is experimental and subject to change in future releases. The automatic +project root detection may not work reliably in all contexts, especially when +projects have complex directory structures or when running in non-standard +environments. For a more explicit and potentially more robust approach, +consider using \code{\link[here:i_am]{here::i_am()}} to declare your project structure, +followed by \code{\link[here:here]{here::here()}} for path construction. See examples for comparison. +} +\details{ +The function uses the following fallback hierarchy to determine the project root: +\itemize{ +\item Quarto environment variables set during Quarto commands (e.g., \verb{quarto render}): +\itemize{ +\item \code{QUARTO_PROJECT_ROOT} environment variable (set by Quarto commands) +\item \code{QUARTO_PROJECT_DIR} environment variable (alternative Quarto variable) +} +\item Fallback to intelligent project root detection using \code{\link[xfun:proj_root]{xfun::proj_root()}} for interactive sessions: +\itemize{ +\item \verb{_quarto.yml} or \verb{_quarto.yaml} (Quarto project files) +\item \code{.vscode} directory (VS Code/Positron workspace) +\item \code{DESCRIPTION} file with \verb{Package:} field (R package or Project) +\item \code{.Rproj} files with \verb{Version:} field (RStudio projects) +} +} + +Last fallback is the current working directory if no project root can be determined. +A warning is issued to alert users that behavior may differ between interactive use and Quarto rendering, +as in this case the computed path may be wrong. +} +\examples{ +\dontrun{ +# Reference a data file from project root +data_path <- quarto::project_path("data", "my_data.csv") + +# Reference a script +script_path <- quarto::project_path("R", "analysis.R") + +# Reference nested directories +output_path <- quarto::project_path("outputs", "figures", "plot.png") + +# Explicitly specify root (overrides automatic detection) +custom_path <- quarto::project_path("data", "file.csv", root = "/path/to/project") + +# Alternative approach using here::i_am() (potentially more robust) +# This approach requires you to declare where you are in the project: +if (requireNamespace("here", quietly = TRUE)) { + # Declare that this document is in the project root or subdirectory + here::i_am("analysis.qmd") # If in project root + # here::i_am("reports/analysis.qmd") # If in subdirectory + + # Now here::here() will work reliably from the project root + data_path_alt <- here::here("data", "my_data.csv") + script_path_alt <- here::here("R", "analysis.R") + output_path_alt <- here::here("outputs", "figures", "plot.png") +} +} + +} +\seealso{ +\itemize{ +\item \code{\link[here:here]{here::here()}} for a similar function that works with R projects +\item \code{\link[=is_running_quarto_project]{is_running_quarto_project()}} to check if quarto is running with a project context +\item \code{\link[xfun:from_root]{xfun::from_root()}} for the underlying path construction +\item \code{\link[xfun:proj_root]{xfun::proj_root()}} for project root detection logic +} +} From 8279274d004876bc0fd7377177757f6a251fe0a8 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 3 Jul 2025 19:43:47 +0200 Subject: [PATCH 02/24] remove .vscode detection --- R/utils-projects.R | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/R/utils-projects.R b/R/utils-projects.R index a4d97fe8..cd7832ad 100644 --- a/R/utils-projects.R +++ b/R/utils-projects.R @@ -24,7 +24,6 @@ #' #' - Fallback to intelligent project root detection using [xfun::proj_root()] for interactive sessions: #' - `_quarto.yml` or `_quarto.yaml` (Quarto project files) -#' - `.vscode` directory (VS Code/Positron workspace) #' - `DESCRIPTION` file with `Package:` field (R package or Project) #' - `.Rproj` files with `Version:` field (RStudio projects) #' @@ -80,8 +79,8 @@ project_path <- function(..., root = NULL) { Sys.getenv("QUARTO_PROJECT_DIR") ) - if (nzchar(quarto_root)) { - root <- quarto_root + root <- if (nzchar(quarto_root)) { + quarto_root } else { # Try to find project root using xfun::proj_root() with extended rules tryCatch( @@ -92,19 +91,17 @@ project_path <- function(..., root = NULL) { # which are only set when running Quarto commands c("_quarto.yml", ""), # Quarto project config c("_quarto.yaml", ""), # Alternative Quarto config - # This is to provide some better fallback than just the working directory - c(".vscode", ""), # VS Code/Positron workspace xfun::root_rules # Default rules (DESCRIPTION, .Rproj) ) proj_root <- xfun::proj_root(rules = extended_rules) - root <- if (!is.null(proj_root)) { + if (!is.null(proj_root)) { proj_root } else { - cli::cli_warn( + cli::cli_warn(c( "Failed to determine project root using {.fun xfun::proj_root}. Using current working directory.", ">" = "This may lead to different behavior interactively vs running Quarto commands." - ) + )) getwd() } }, @@ -114,16 +111,25 @@ project_path <- function(..., root = NULL) { "Failed to determine project root: {e$message}. Using current working directory as a fallback.", ">" = "This may lead to different behavior interactively vs running Quarto commands." )) - root <- getwd() + getwd() # Return the working directory } ) } } # Use xfun::from_root for better path handling - path <- tryCatch( - xfun::from_root(..., root = root, error = FALSE), - error = function(e) file.path(root, ...) + path <- rlang::try_fetch( + xfun::from_root(..., root = root, error = TRUE), + error = function(e) { + rlang::abort( + c( + "Failed to construct project path", + ">" = "Ensure you are using valid path components." + ), + parent = e, + call = rlang::caller_env() + ) + } ) path } From 3a389c42ca9a16218748bd8d59ad0cbd1421da42 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 3 Jul 2025 19:44:19 +0200 Subject: [PATCH 03/24] Add tests --- tests/testthat/test-utils-projects.R | 221 +++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 tests/testthat/test-utils-projects.R diff --git a/tests/testthat/test-utils-projects.R b/tests/testthat/test-utils-projects.R new file mode 100644 index 00000000..3d34dec6 --- /dev/null +++ b/tests/testthat/test-utils-projects.R @@ -0,0 +1,221 @@ +test_that("project_path() works with explicit root", { + temp_dir <- withr::local_tempdir() + withr::local_dir(temp_dir) + expect_identical( + project_path("data", "file.csv", root = temp_dir), + file.path("data", "file.csv") + ) + expect_identical( + project_path("outputs", "figures", "plot.png", root = temp_dir), + file.path("outputs", "figures", "plot.png") + ) + expect_identical(project_path(root = temp_dir), ".") +}) + +test_that("project_path() uses Quarto environment variables", { + temp_dir <- withr::local_tempdir() + withr::local_dir(temp_dir) + dir.create("project") + withr::local_envvar( + QUARTO_PROJECT_ROOT = xfun::normalize_path(file.path(temp_dir, "project")) + ) + expect_identical( + project_path("data", "file.csv"), + file.path("project", "data", "file.csv") + ) + withr::local_envvar( + QUARTO_PROJECT_ROOT = "", + QUARTO_PROJECT_DIR = xfun::normalize_path(file.path(temp_dir, "project")) + ) + expect_identical( + project_path("data", "file.csv"), + file.path("project", "data", "file.csv") + ) +}) + +test_that("project_path() detects Quarto project files", { + skip_if_no_quarto() + + project_dir <- local_quarto_project(type = "blog") + # simulate running from a post directory withing .qmd + post_dir <- file.path(project_dir, "posts", "welcome") + withr::local_dir(post_dir) + # data is at root of the project and path should be relative to that + expect_identical( + project_path("data", "file.csv"), + "../../data/file.csv" + ) +}) + +test_that("project_path() detects R package DESCRIPTION", { + temp_dir <- withr::local_tempdir() + desc_file <- file.path(temp_dir, "DESCRIPTION") + writeLines(c("Package: testpkg", "Version: 1.0.0"), desc_file) + withr::local_dir(temp_dir) + dir.create("reports") + withr::local_dir("reports") + expect_identical( + project_path("R", "functions.R"), + "../R/functions.R" + ) +}) + +test_that("project_path() detects .Rproj files", { + temp_dir <- withr::local_tempdir() + rproj_file <- file.path(temp_dir, "test.Rproj") + writeLines("Version: 1.0", rproj_file) + withr::local_dir(temp_dir) + dir.create("reports") + withr::local_dir("reports") + expect_identical( + project_path("analysis", "script.R"), + "../analysis/script.R" + ) +}) + +test_that("project_path() falls back to working directory with warning", { + temp_dir <- withr::local_tempdir() + withr::local_dir(temp_dir) + withr::local_envvar( + QUARTO_PROJECT_ROOT = "", + QUARTO_PROJECT_DIR = "" + ) + + expect_warning( + expect_identical( + project_path("data", "file.csv"), + file.path("data", "file.csv") + ), + "Failed to determine project root" + ) +}) + +test_that("project_path() handles xfun::proj_root() errors gracefully", { + temp_dir <- withr::local_tempdir() + + # Mock xfun::proj_root to throw an error + local_mocked_bindings( + proj_root = function(path = ".", rules = xfun::root_rules) { + stop("Test error") + }, + .package = "xfun" + ) + + withr::local_dir(temp_dir) + withr::local_envvar( + QUARTO_PROJECT_ROOT = "", + QUARTO_PROJECT_DIR = "" + ) + + expect_warning( + expect_identical( + project_path("data", "file.csv"), + file.path("data", "file.csv") + ), + "Failed to determine project root: Test error" + ) +}) + +test_that("is_running_quarto_project() detects environment variables", { + withr::with_envvar( + list( + QUARTO_PROJECT_ROOT = "", + QUARTO_PROJECT_DIR = "" + ), + expect_false(is_running_quarto_project()) + ) + + withr::with_envvar( + list( + QUARTO_PROJECT_ROOT = "/some/path", + QUARTO_PROJECT_DIR = "" + ), + expect_true(is_running_quarto_project()) + ) + + withr::with_envvar( + list( + QUARTO_PROJECT_ROOT = "", + QUARTO_PROJECT_DIR = "/some/path" + ), + expect_true(is_running_quarto_project()) + ) + + withr::with_envvar( + list( + QUARTO_PROJECT_ROOT = "/path1", + QUARTO_PROJECT_DIR = "/path2" + ), + expect_true(is_running_quarto_project()) + ) +}) + +test_that("is_quarto_project() detects Quarto project files", { + skip_if_no_quarto() + + temp_dir <- withr::local_tempdir() + expect_false(is_quarto_project(temp_dir)) + + project_dir <- local_quarto_project(type = "default") + expect_true(is_quarto_project(project_dir)) + + withr::local_dir(project_dir) + expect_true(is_quarto_project()) +}) + +test_that("is_quarto_project() works with _quarto.yaml", { + temp_dir <- withr::local_tempdir() + + quarto_yaml <- file.path(temp_dir, "_quarto.yaml") + writeLines("project:", quarto_yaml) + + expect_true(is_quarto_project(temp_dir)) + + withr::local_dir(temp_dir) + expect_true(is_quarto_project()) +}) + +test_that("is_quarto_project() handles errors gracefully", { + # Mock xfun::proj_root to throw an error + local_mocked_bindings( + proj_root = function(path = ".", rules = xfun::root_rules) { + stop("Test error") + }, + .package = "xfun" + ) + + temp_dir <- withr::local_tempdir() + expect_false(is_quarto_project(temp_dir)) +}) + +test_that("project_path() prioritizes environment variables over file detection", { + skip_if_no_quarto() + + temp1 <- withr::local_tempdir() + temp2 <- withr::local_tempdir() + + quarto_create_project( + name = "test_project", + type = "default", + dir = temp1, + no_prompt = TRUE, + quiet = TRUE + ) + + withr::local_envvar( + QUARTO_PROJECT_ROOT = "", + QUARTO_PROJECT_DIR = "" + ) + project_dir <- file.path(temp1, "test_project") + dir.create(file.path(project_dir, "subfolder")) + withr::local_dir(file.path(project_dir, "subfolder")) + + expect_identical(project_path("test.txt"), "../test.txt") + + # With env var, should use env var instead + withr::local_envvar(QUARTO_PROJECT_ROOT = file.path(project_dir, "subfolder")) + expect_identical( + project_path("test.txt"), + "test.txt" + ) +}) From 73eee7875328e42e3ad68900f168a49e7857ce31 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 13:19:00 +0200 Subject: [PATCH 04/24] rename function and update help page --- NAMESPACE | 4 +- R/utils-projects.R | 124 +++++++++--------- ...quarto_project.Rd => find_project_root.Rd} | 16 +-- ...project.Rd => get_running_project_root.Rd} | 24 ++-- man/project_path.Rd | 56 ++++---- 5 files changed, 118 insertions(+), 106 deletions(-) rename man/{is_quarto_project.Rd => find_project_root.Rd} (78%) rename man/{is_running_quarto_project.Rd => get_running_project_root.Rd} (50%) diff --git a/NAMESPACE b/NAMESPACE index 68a74556..f53d9d96 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,8 +2,8 @@ export(add_spin_preamble) export(check_newer_version) -export(is_quarto_project) -export(is_running_quarto_project) +export(find_project_root) +export(get_running_project_root) export(is_using_quarto) export(new_blog_post) export(project_path) diff --git a/R/utils-projects.R b/R/utils-projects.R index cd7832ad..08cbeaa2 100644 --- a/R/utils-projects.R +++ b/R/utils-projects.R @@ -34,39 +34,50 @@ #' @param ... Character vectors of path components to be joined #' @param root Project root directory. If `NULL` (default), automatic detection #' is used following the hierarchy described above -#' @return A character vector of the normalized file path relative to the project root +#' @return A character vector of the normalized file path relative to the project root. #' #' @examples #' \dontrun{ -#' # Reference a data file from project root -#' data_path <- quarto::project_path("data", "my_data.csv") +#' # Create a dummy Quarto project structure for example +#' tmpdir <- tempfile("quarto_project") +#' dir.create(tmpdir) +#' quarto::quarto_create_project('test project', type = 'blog', dir = tmpdir, no_prompt = TRUE, quiet = TRUE) +#' project_dir <- file.path(tmpdir, "test project") +#' +#' # Simulate working within a blog post +#' xfun::in_dir( +#' dir = file.path(project_dir, "posts", "welcome"), { +#' +#' # Reference a data file from project root +#' # ../../data/my_data.csv +#' quarto::project_path("data", "my_data.csv") +#' +#' # Reference a script from project root +#' # ../../R/analysis.R +#' quarto::project_path("R", "analysis.R") +#' +#' # Explicitly specify root (overrides automatic detection) +#' # ../../data/file.csv +#' quarto::project_path("data", "file.csv", root = "../..") +#' +#' # Alternative approach using here::i_am() (potentially more robust) +#' # This approach requires you to declare where you are in the project: +#' if (requireNamespace("here", quietly = TRUE)) { +#' # Declare that this document is in the project root or subdirectory +#' here::i_am("posts/welcome/index.qmd") +#' +#' # Now here::here() will work reliably from the project root +#' here::here("data", "my_data.csv") +#' here::here("R", "analysis.R") +#' } +#' }) #' -#' # Reference a script -#' script_path <- quarto::project_path("R", "analysis.R") -#' -#' # Reference nested directories -#' output_path <- quarto::project_path("outputs", "figures", "plot.png") -#' -#' # Explicitly specify root (overrides automatic detection) -#' custom_path <- quarto::project_path("data", "file.csv", root = "/path/to/project") -#' -#' # Alternative approach using here::i_am() (potentially more robust) -#' # This approach requires you to declare where you are in the project: -#' if (requireNamespace("here", quietly = TRUE)) { -#' # Declare that this document is in the project root or subdirectory -#' here::i_am("analysis.qmd") # If in project root -#' # here::i_am("reports/analysis.qmd") # If in subdirectory -#' -#' # Now here::here() will work reliably from the project root -#' data_path_alt <- here::here("data", "my_data.csv") -#' script_path_alt <- here::here("R", "analysis.R") -#' output_path_alt <- here::here("outputs", "figures", "plot.png") -#' } #' } #' #' @seealso -#' * [here::here()] for a similar function that works with R projects -#' * [is_running_quarto_project()] to check if quarto is running with a project context +#' * [here::here()] and [here::i_am()] for a similar function that works with R projects +#' * [find_project_root()] to search for Quarto Project configuration in parents directories +#' * [get_running_project_root()] for detecting the project root in Quarto commands #' * [xfun::from_root()] for the underlying path construction #' * [xfun::proj_root()] for project root detection logic #' @@ -74,12 +85,9 @@ project_path <- function(..., root = NULL) { if (is.null(root)) { # Try Quarto project environment variables first - quarto_root <- Sys.getenv( - "QUARTO_PROJECT_ROOT", - Sys.getenv("QUARTO_PROJECT_DIR") - ) + quarto_root <- get_running_project_root() - root <- if (nzchar(quarto_root)) { + root <- if (!is.null(quarto_root) && nzchar(quarto_root)) { quarto_root } else { # Try to find project root using xfun::proj_root() with extended rules @@ -134,41 +142,44 @@ project_path <- function(..., root = NULL) { path } -#' Check if running within a Quarto project context +#' Get the root of the currently running Quarto project #' #' @description -#' This function checks if the current R session is running within a Quarto -#' project context by detecting Quarto project environment variables. +#' This function is to be used inside cells and will return the project root +#' when doing [quarto_render()] by detecting Quarto project environment variables. #' #' @details #' Quarto sets `QUARTO_PROJECT_ROOT` and `QUARTO_PROJECT_DIR` environment #' variables when executing commands within a Quarto project context (e.g., #' `quarto render`, `quarto preview`). This function detects their presence. #' -#' Note that this function will return `FALSE` when running code interactively +#' Note that this function will return `NULL` when running code interactively #' in an IDE (even within a Quarto project directory), as these specific #' environment variables are only set during Quarto command execution. #' -#' @return Logical indicating if Quarto project environment variables are set +#' @return Character Quarto project root path from set environment variables. #' #' @seealso -#' * [is_quarto_project()] for checking Quarto project structure +#' * [find_project_root()] for finding the Quarto project root directory #' * [project_path()] for constructing paths relative to the project root #' @examples #' \dontrun{ #' # This will be TRUE during `quarto render` in a project -#' is_running_quarto_project() +#' get_running_project_root() #' #' # This will be FALSE when not running during `quarto_render` (e.g. interactively) -#' is_running_quarto_project() +#' get_running_project_root() #' } #' @export -is_running_quarto_project <- function() { - nzchar(Sys.getenv("QUARTO_PROJECT_ROOT")) || - nzchar(Sys.getenv("QUARTO_PROJECT_DIR")) +get_running_project_root <- function() { + root <- Sys.getenv("QUARTO_PROJECT_ROOT", Sys.getenv("QUARTO_PROJECT_DIR")) + if (!nzchar(root)) { + return() + } + root } -#' Check if working within a Quarto project structure +#' Find the root of a Quarto project #' #' @description #' This function checks if the current working directory is within a Quarto @@ -179,18 +190,18 @@ is_running_quarto_project <- function() { #' @param path Character. Path to check for Quarto project files. Defaults to #' current working directory. #' -#' @return Logical indicating if a Quarto project structure is detected +#' @return Character Path of the project root directory if found, or `NULL` #' #' @examplesIf quarto_available() #' dir <- tempfile() #' dir.create(dir) -#' is_quarto_project(dir) +#' find_project_root(dir) #' quarto_create_project(dir) -#' is_quarto_project(dir) +#' find_project_root(dir) #' #' xfun::in_dir(dir, #' # Check if current directory is in a Quarto project -#' is_quarto_project() +#' !is.null(find_project_root()) #' ) #' # clean up #' unlink(dir, recursive = TRUE) @@ -198,19 +209,10 @@ is_running_quarto_project <- function() { #' #' @seealso [is_running_quarto_project()] for detecting active Quarto rendering #' @export -is_quarto_project <- function(path = ".") { - tryCatch( - { - quarto_rules <- rbind( - c("_quarto.yml", ""), - c("_quarto.yaml", "") - ) - - proj_root <- xfun::proj_root(path = path, rules = quarto_rules) - !is.null(proj_root) - }, - error = function(e) { - FALSE - } +find_project_root <- function(path = ".") { + quarto_rules <- rbind( + c("_quarto.yml", ""), + c("_quarto.yaml", "") ) + xfun::proj_root(path = path, rules = quarto_rules) } diff --git a/man/is_quarto_project.Rd b/man/find_project_root.Rd similarity index 78% rename from man/is_quarto_project.Rd rename to man/find_project_root.Rd index 47baec4a..622db3da 100644 --- a/man/is_quarto_project.Rd +++ b/man/find_project_root.Rd @@ -1,17 +1,17 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils-projects.R -\name{is_quarto_project} -\alias{is_quarto_project} -\title{Check if working within a Quarto project structure} +\name{find_project_root} +\alias{find_project_root} +\title{Find the root of a Quarto project} \usage{ -is_quarto_project(path = ".") +find_project_root(path = ".") } \arguments{ \item{path}{Character. Path to check for Quarto project files. Defaults to current working directory.} } \value{ -Logical indicating if a Quarto project structure is detected +Character Path of the project root directory if found, or \code{NULL} } \description{ This function checks if the current working directory is within a Quarto @@ -23,13 +23,13 @@ interactive sessions. \dontshow{if (quarto_available()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} dir <- tempfile() dir.create(dir) -is_quarto_project(dir) +find_project_root(dir) quarto_create_project(dir) -is_quarto_project(dir) +find_project_root(dir) xfun::in_dir(dir, # Check if current directory is in a Quarto project - is_quarto_project() + !is.null(find_project_root()) ) # clean up unlink(dir, recursive = TRUE) diff --git a/man/is_running_quarto_project.Rd b/man/get_running_project_root.Rd similarity index 50% rename from man/is_running_quarto_project.Rd rename to man/get_running_project_root.Rd index 359a031a..b712c8e4 100644 --- a/man/is_running_quarto_project.Rd +++ b/man/get_running_project_root.Rd @@ -1,39 +1,39 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils-projects.R -\name{is_running_quarto_project} -\alias{is_running_quarto_project} -\title{Check if running within a Quarto project context} +\name{get_running_project_root} +\alias{get_running_project_root} +\title{Get the root of the currently running Quarto project} \usage{ -is_running_quarto_project() +get_running_project_root() } \value{ -Logical indicating if Quarto project environment variables are set +Character Quarto project root path from set environment variables. } \description{ -This function checks if the current R session is running within a Quarto -project context by detecting Quarto project environment variables. +This function is to be used inside cells and will return the project root +when doing \code{\link[=quarto_render]{quarto_render()}} by detecting Quarto project environment variables. } \details{ Quarto sets \code{QUARTO_PROJECT_ROOT} and \code{QUARTO_PROJECT_DIR} environment variables when executing commands within a Quarto project context (e.g., \verb{quarto render}, \verb{quarto preview}). This function detects their presence. -Note that this function will return \code{FALSE} when running code interactively +Note that this function will return \code{NULL} when running code interactively in an IDE (even within a Quarto project directory), as these specific environment variables are only set during Quarto command execution. } \examples{ \dontrun{ # This will be TRUE during `quarto render` in a project -is_running_quarto_project() +get_running_project_root() -# This will be FALSE when running interactively -is_running_quarto_project() +# This will be FALSE when not running during `quarto_render` (e.g. interactively) +get_running_project_root() } } \seealso{ \itemize{ -\item \code{\link[=is_quarto_project]{is_quarto_project()}} for checking Quarto project structure +\item \code{\link[=find_project_root]{find_project_root()}} for finding the Quarto project root directory \item \code{\link[=project_path]{project_path()}} for constructing paths relative to the project root } } diff --git a/man/project_path.Rd b/man/project_path.Rd index 4ed9d255..d1e04e5b 100644 --- a/man/project_path.Rd +++ b/man/project_path.Rd @@ -13,7 +13,7 @@ project_path(..., root = NULL) is used following the hierarchy described above} } \value{ -A character vector of the normalized file path relative to the project root +A character vector of the normalized file path relative to the project root. } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} @@ -41,7 +41,6 @@ The function uses the following fallback hierarchy to determine the project root \item Fallback to intelligent project root detection using \code{\link[xfun:proj_root]{xfun::proj_root()}} for interactive sessions: \itemize{ \item \verb{_quarto.yml} or \verb{_quarto.yaml} (Quarto project files) -\item \code{.vscode} directory (VS Code/Positron workspace) \item \code{DESCRIPTION} file with \verb{Package:} field (R package or Project) \item \code{.Rproj} files with \verb{Version:} field (RStudio projects) } @@ -53,37 +52,48 @@ as in this case the computed path may be wrong. } \examples{ \dontrun{ -# Reference a data file from project root -data_path <- quarto::project_path("data", "my_data.csv") +# Create a dummy Quarto project structure for example +tmpdir <- tempfile("quarto_project") +dir.create(tmpdir) +quarto::quarto_create_project('test project', type = 'blog', dir = tmpdir, no_prompt = TRUE, quiet = TRUE) +project_dir <- file.path(tmpdir, "test project") -# Reference a script -script_path <- quarto::project_path("R", "analysis.R") +# Simulate working within a blog post +xfun::in_dir( + dir = file.path(project_dir, "posts", "welcome"), { -# Reference nested directories -output_path <- quarto::project_path("outputs", "figures", "plot.png") + # Reference a data file from project root + # ../../data/my_data.csv + quarto::project_path("data", "my_data.csv") -# Explicitly specify root (overrides automatic detection) -custom_path <- quarto::project_path("data", "file.csv", root = "/path/to/project") + # Reference a script from project root + # ../../R/analysis.R + quarto::project_path("R", "analysis.R") -# Alternative approach using here::i_am() (potentially more robust) -# This approach requires you to declare where you are in the project: -if (requireNamespace("here", quietly = TRUE)) { - # Declare that this document is in the project root or subdirectory - here::i_am("analysis.qmd") # If in project root - # here::i_am("reports/analysis.qmd") # If in subdirectory + # Explicitly specify root (overrides automatic detection) + # ../../data/file.csv + quarto::project_path("data", "file.csv", root = "../..") + + # Alternative approach using here::i_am() (potentially more robust) + # This approach requires you to declare where you are in the project: + if (requireNamespace("here", quietly = TRUE)) { + # Declare that this document is in the project root or subdirectory + here::i_am("posts/welcome/index.qmd") + + # Now here::here() will work reliably from the project root + here::here("data", "my_data.csv") + here::here("R", "analysis.R") + } +}) - # Now here::here() will work reliably from the project root - data_path_alt <- here::here("data", "my_data.csv") - script_path_alt <- here::here("R", "analysis.R") - output_path_alt <- here::here("outputs", "figures", "plot.png") -} } } \seealso{ \itemize{ -\item \code{\link[here:here]{here::here()}} for a similar function that works with R projects -\item \code{\link[=is_running_quarto_project]{is_running_quarto_project()}} to check if quarto is running with a project context +\item \code{\link[here:here]{here::here()}} and \code{\link[here:i_am]{here::i_am()}} for a similar function that works with R projects +\item \code{\link[=find_project_root]{find_project_root()}} to search for Quarto Project configuration in parents directories +\item \code{\link[=get_running_project_root]{get_running_project_root()}} for detecting the project root in Quarto commands \item \code{\link[xfun:from_root]{xfun::from_root()}} for the underlying path construction \item \code{\link[xfun:proj_root]{xfun::proj_root()}} for project root detection logic } From 5abbb41540e45969b2b84117dc23a06a54a6513b Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 13:29:54 +0200 Subject: [PATCH 05/24] Update tests --- tests/testthat/test-utils-projects.R | 53 +++++++++++++++------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/tests/testthat/test-utils-projects.R b/tests/testthat/test-utils-projects.R index 3d34dec6..3e0570cb 100644 --- a/tests/testthat/test-utils-projects.R +++ b/tests/testthat/test-utils-projects.R @@ -122,7 +122,7 @@ test_that("is_running_quarto_project() detects environment variables", { QUARTO_PROJECT_ROOT = "", QUARTO_PROJECT_DIR = "" ), - expect_false(is_running_quarto_project()) + expect_null(get_running_project_root()) ) withr::with_envvar( @@ -130,7 +130,7 @@ test_that("is_running_quarto_project() detects environment variables", { QUARTO_PROJECT_ROOT = "/some/path", QUARTO_PROJECT_DIR = "" ), - expect_true(is_running_quarto_project()) + expect_identical(get_running_project_root(), "/some/path") ) withr::with_envvar( @@ -138,7 +138,7 @@ test_that("is_running_quarto_project() detects environment variables", { QUARTO_PROJECT_ROOT = "", QUARTO_PROJECT_DIR = "/some/path" ), - expect_true(is_running_quarto_project()) + expect_identical(get_running_project_root(), "/some/path") ) withr::with_envvar( @@ -146,46 +146,49 @@ test_that("is_running_quarto_project() detects environment variables", { QUARTO_PROJECT_ROOT = "/path1", QUARTO_PROJECT_DIR = "/path2" ), - expect_true(is_running_quarto_project()) + expect_identical(get_running_project_root(), "/path1") ) }) -test_that("is_quarto_project() detects Quarto project files", { +test_that("find_project_root() detects Quarto project files", { skip_if_no_quarto() temp_dir <- withr::local_tempdir() - expect_false(is_quarto_project(temp_dir)) + expect_null(find_project_root(temp_dir)) - project_dir <- local_quarto_project(type = "default") - expect_true(is_quarto_project(project_dir)) + project_dir <- local_quarto_project("test-project", type = "default") + expect_match( + find_project_root(project_dir), + "quarto-tests-project-.*/test-project$" + ) - withr::local_dir(project_dir) - expect_true(is_quarto_project()) + withr::with_dir( + project_dir, + expect_match( + find_project_root(), + "quarto-tests-project-.*/test-project$" + ) + ) }) -test_that("is_quarto_project() works with _quarto.yaml", { +test_that("find_project_root() works with _quarto.yaml", { temp_dir <- withr::local_tempdir() quarto_yaml <- file.path(temp_dir, "_quarto.yaml") - writeLines("project:", quarto_yaml) + writeLines("project:\n type: default", quarto_yaml) - expect_true(is_quarto_project(temp_dir)) + expect_identical(find_project_root(temp_dir), xfun::normalize_path(temp_dir)) withr::local_dir(temp_dir) - expect_true(is_quarto_project()) -}) + expect_identical(find_project_root(), xfun::normalize_path(temp_dir)) -test_that("is_quarto_project() handles errors gracefully", { - # Mock xfun::proj_root to throw an error - local_mocked_bindings( - proj_root = function(path = ".", rules = xfun::root_rules) { - stop("Test error") - }, - .package = "xfun" + dir.create("subfolder") + expect_identical( + find_project_root("subfolder"), + xfun::normalize_path(temp_dir) ) - - temp_dir <- withr::local_tempdir() - expect_false(is_quarto_project(temp_dir)) + withr::local_dir("subfolder") + expect_identical(find_project_root(), xfun::normalize_path(temp_dir)) }) test_that("project_path() prioritizes environment variables over file detection", { From 27a1c4acaa59ecb69a2d327519a470137c95d10e Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 13:47:01 +0200 Subject: [PATCH 06/24] Add example in help page body --- R/utils-projects.R | 28 ++++++++++++++++++++++++---- man/get_running_project_root.Rd | 17 +++++++++++++---- man/project_path.Rd | 13 +++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/R/utils-projects.R b/R/utils-projects.R index 08cbeaa2..16a532a6 100644 --- a/R/utils-projects.R +++ b/R/utils-projects.R @@ -31,6 +31,18 @@ #' A warning is issued to alert users that behavior may differ between interactive use and Quarto rendering, #' as in this case the computed path may be wrong. #' +#' @section Use in Quarto document cells: +#' +#' This function is particularly useful in Quarto document cells where you want to +#' use a path relative to the project root dynamically during rendering. +#' +#' ````markdown +#' ```{r}`r ''` +#' # Get a csv path from data directory in the Quarto project root +#' data <- project_path("data", "my_data.csv") +#' ``` +#' ```` +#' #' @param ... Character vectors of path components to be joined #' @param root Project root directory. If `NULL` (default), automatic detection #' is used following the hierarchy described above @@ -157,6 +169,18 @@ project_path <- function(..., root = NULL) { #' in an IDE (even within a Quarto project directory), as these specific #' environment variables are only set during Quarto command execution. #' +#' @section Use in Quarto document cells: +#' +#' This function is particularly useful in Quarto document cells where you want to +#' get the project root path dynamically during rendering. Cell example: +#' +#' ````markdown +#' ```{r}`r ''` +#' # Get the project root path +#' project_root <- get_running_project_root() +#' ``` +#' ```` +#' #' @return Character Quarto project root path from set environment variables. #' #' @seealso @@ -164,10 +188,6 @@ project_path <- function(..., root = NULL) { #' * [project_path()] for constructing paths relative to the project root #' @examples #' \dontrun{ -#' # This will be TRUE during `quarto render` in a project -#' get_running_project_root() -#' -#' # This will be FALSE when not running during `quarto_render` (e.g. interactively) #' get_running_project_root() #' } #' @export diff --git a/man/get_running_project_root.Rd b/man/get_running_project_root.Rd index b712c8e4..486191e6 100644 --- a/man/get_running_project_root.Rd +++ b/man/get_running_project_root.Rd @@ -22,12 +22,21 @@ Note that this function will return \code{NULL} when running code interactively in an IDE (even within a Quarto project directory), as these specific environment variables are only set during Quarto command execution. } +\section{Use in Quarto document cells}{ + + +This function is particularly useful in Quarto document cells where you want to +get the project root path dynamically during rendering. Cell example: + +\if{html}{\out{
}}\preformatted{```\{r\}`r ''` + # Get the project root path + project_root <- get_running_project_root() +``` +}\if{html}{\out{
}} +} + \examples{ \dontrun{ -# This will be TRUE during `quarto render` in a project -get_running_project_root() - -# This will be FALSE when not running during `quarto_render` (e.g. interactively) get_running_project_root() } } diff --git a/man/project_path.Rd b/man/project_path.Rd index d1e04e5b..2384c678 100644 --- a/man/project_path.Rd +++ b/man/project_path.Rd @@ -50,6 +50,19 @@ Last fallback is the current working directory if no project root can be determi A warning is issued to alert users that behavior may differ between interactive use and Quarto rendering, as in this case the computed path may be wrong. } +\section{Use in Quarto document cells}{ + + +This function is particularly useful in Quarto document cells where you want to +use a path relative to the project root dynamically during rendering. + +\if{html}{\out{
}}\preformatted{```\{r\}`r ''` + # Get a csv path from data directory in the Quarto project root + data <- project_path("data", "my_data.csv") +``` +}\if{html}{\out{
}} +} + \examples{ \dontrun{ # Create a dummy Quarto project structure for example From 854d700d6d55c330cb6b3e697ff9b82155be39b8 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 13:48:44 +0200 Subject: [PATCH 07/24] Add NEWS bullet --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 75a535a3..ce5110ab 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # quarto (development version) +- Added `project_path()`, `get_running_project_root()`, and `find_project_root()` functions for Quarto-aware project path construction. These functions provide a consistent way to reference files relative to the project root, working both during Quarto rendering (using `QUARTO_PROJECT_ROOT` environment variables) and in interactive sessions (using intelligent project detection). The `project_path()` function is particularly useful in Quarto document cells where you need to reference data files or scripts from the project root regardless of the document's location in subdirectories (#180). + - `quarto_preview()` now explicitly returns the preview server URL (invisibly) and documents this behavior. This enables programmatic workflows such as taking screenshots with **webshot2** or passing the URL to other automation tools (thanks, @cwickham, #233). - Added NA value detection in YAML processing to prevent silent failures when passing R's `NA` values to Quarto CLI. Functions `as_yaml()` and `write_yaml()` now validate for NA values and provide clear error messages with actionable suggestions. This addresses issues where R's `NA` values get converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values, because they are not supported in YAML 1.2 spec. This is to help users handle missing data appropriately before passing to Quarto (#168). From de0ebecaec452257ffc16c21542d65282c8b2ee9 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 13:58:45 +0200 Subject: [PATCH 08/24] Add to pkgdown --- _pkgdown.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 3de383ea..5d26cb18 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -35,6 +35,9 @@ reference: contents: - quarto_create_project - new_blog_post + - project_path + - find_project_root + - get_running_project_root - title: "Configuration" desc: > From aebad44622f9df2fc8203981dc3ca35490772a4f Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 14:03:48 +0200 Subject: [PATCH 09/24] Update test --- tests/testthat/test-utils-projects.R | 34 +++++++++++++++++----------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/tests/testthat/test-utils-projects.R b/tests/testthat/test-utils-projects.R index 3e0570cb..169cb45f 100644 --- a/tests/testthat/test-utils-projects.R +++ b/tests/testthat/test-utils-projects.R @@ -16,20 +16,28 @@ test_that("project_path() uses Quarto environment variables", { temp_dir <- withr::local_tempdir() withr::local_dir(temp_dir) dir.create("project") - withr::local_envvar( - QUARTO_PROJECT_ROOT = xfun::normalize_path(file.path(temp_dir, "project")) - ) - expect_identical( - project_path("data", "file.csv"), - file.path("project", "data", "file.csv") - ) - withr::local_envvar( - QUARTO_PROJECT_ROOT = "", - QUARTO_PROJECT_DIR = xfun::normalize_path(file.path(temp_dir, "project")) + withr::with_envvar( + list( + QUARTO_PROJECT_ROOT = xfun::normalize_path(file.path( + temp_dir, + "project" + )), + QUARTO_PROJECT_DIR = "" + ), + expect_identical( + project_path("data", "file.csv"), + file.path("project", "data", "file.csv") + ) ) - expect_identical( - project_path("data", "file.csv"), - file.path("project", "data", "file.csv") + withr::with_envvar( + list( + QUARTO_PROJECT_ROOT = "", + QUARTO_PROJECT_DIR = xfun::normalize_path(file.path(temp_dir, "project")) + ), + expect_identical( + project_path("data", "file.csv"), + file.path("project", "data", "file.csv") + ) ) }) From 8e6b0f465e907f47cf5a9fbea00d4b6c3fb2d3ae Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 14:08:57 +0200 Subject: [PATCH 10/24] Fix empty dir logic --- R/use.R | 4 ++++ R/utils.R | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/R/use.R b/R/use.R index 6cb98637..fc4f882d 100644 --- a/R/use.R +++ b/R/use.R @@ -36,6 +36,10 @@ quarto_use_template <- function( ) { rlang::check_required(template) + if (!fs::dir_exists(dir)) { + fs::dir_create(dir) + } + if (!is_empty_dir(dir) && quarto_available("1.5.15")) { cli::cli_abort(c( "{.arg dir} must be an empty directory.", diff --git a/R/utils.R b/R/utils.R index 66643837..5f8abe85 100644 --- a/R/utils.R +++ b/R/utils.R @@ -106,7 +106,10 @@ has_internet <- function(host = "https://www.google.com") { is_empty_dir <- function(dir) { if (!dir.exists(dir)) { - return(FALSE) + rlang::warn( + "Directory {.path {dir}} does not exist. Assuming it is empty." + ) + return(TRUE) } files <- list.files(dir, all.files = TRUE, no.. = TRUE) length(files) == 0 From 0abb385a9c3440616c5201646ef30644818539ed Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 14:18:48 +0200 Subject: [PATCH 11/24] correct documentation --- R/utils-projects.R | 2 +- man/find_project_root.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/utils-projects.R b/R/utils-projects.R index 16a532a6..5cec2e37 100644 --- a/R/utils-projects.R +++ b/R/utils-projects.R @@ -227,7 +227,7 @@ get_running_project_root <- function() { #' unlink(dir, recursive = TRUE) #' #' -#' @seealso [is_running_quarto_project()] for detecting active Quarto rendering +#' @seealso [get_running_project_root()] for detecting active Quarto rendering #' @export find_project_root <- function(path = ".") { quarto_rules <- rbind( diff --git a/man/find_project_root.Rd b/man/find_project_root.Rd index 622db3da..3504605b 100644 --- a/man/find_project_root.Rd +++ b/man/find_project_root.Rd @@ -37,5 +37,5 @@ unlink(dir, recursive = TRUE) \dontshow{\}) # examplesIf} } \seealso{ -\code{\link[=is_running_quarto_project]{is_running_quarto_project()}} for detecting active Quarto rendering +\code{\link[=get_running_project_root]{get_running_project_root()}} for detecting active Quarto rendering } From 51ebd9b0cd555121bf2923c4f0082461585ea718 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 14:18:56 +0200 Subject: [PATCH 12/24] Add a check to debug --- tests/testthat/test-utils-projects.R | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/testthat/test-utils-projects.R b/tests/testthat/test-utils-projects.R index 169cb45f..1553a9f9 100644 --- a/tests/testthat/test-utils-projects.R +++ b/tests/testthat/test-utils-projects.R @@ -34,10 +34,17 @@ test_that("project_path() uses Quarto environment variables", { QUARTO_PROJECT_ROOT = "", QUARTO_PROJECT_DIR = xfun::normalize_path(file.path(temp_dir, "project")) ), - expect_identical( - project_path("data", "file.csv"), - file.path("project", "data", "file.csv") - ) + { + expect_identical( + get_running(), + xfun::normalize_path(file.path(temp_dir, "project")) + ) + + expect_identical( + project_path("data", "file.csv"), + file.path("project", "data", "file.csv") + ) + } ) }) From 593f66e4b987d508deb54926ca5e1730c77d813c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 14:21:55 +0200 Subject: [PATCH 13/24] with_envvar uses named character --- tests/testthat/test-utils-projects.R | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/testthat/test-utils-projects.R b/tests/testthat/test-utils-projects.R index 1553a9f9..0bed62d2 100644 --- a/tests/testthat/test-utils-projects.R +++ b/tests/testthat/test-utils-projects.R @@ -17,7 +17,7 @@ test_that("project_path() uses Quarto environment variables", { withr::local_dir(temp_dir) dir.create("project") withr::with_envvar( - list( + c( QUARTO_PROJECT_ROOT = xfun::normalize_path(file.path( temp_dir, "project" @@ -30,13 +30,13 @@ test_that("project_path() uses Quarto environment variables", { ) ) withr::with_envvar( - list( + c( QUARTO_PROJECT_ROOT = "", QUARTO_PROJECT_DIR = xfun::normalize_path(file.path(temp_dir, "project")) ), { expect_identical( - get_running(), + get_running_project_root(), xfun::normalize_path(file.path(temp_dir, "project")) ) @@ -133,7 +133,7 @@ test_that("project_path() handles xfun::proj_root() errors gracefully", { test_that("is_running_quarto_project() detects environment variables", { withr::with_envvar( - list( + c( QUARTO_PROJECT_ROOT = "", QUARTO_PROJECT_DIR = "" ), @@ -141,7 +141,7 @@ test_that("is_running_quarto_project() detects environment variables", { ) withr::with_envvar( - list( + c( QUARTO_PROJECT_ROOT = "/some/path", QUARTO_PROJECT_DIR = "" ), @@ -149,7 +149,7 @@ test_that("is_running_quarto_project() detects environment variables", { ) withr::with_envvar( - list( + c( QUARTO_PROJECT_ROOT = "", QUARTO_PROJECT_DIR = "/some/path" ), @@ -157,7 +157,7 @@ test_that("is_running_quarto_project() detects environment variables", { ) withr::with_envvar( - list( + c( QUARTO_PROJECT_ROOT = "/path1", QUARTO_PROJECT_DIR = "/path2" ), From 96db907a09d02a2b01a9c82f6c2ceebcbef3cc71 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 14:33:42 +0200 Subject: [PATCH 14/24] Unset var is NA in with_envvar --- tests/testthat/test-utils-projects.R | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/testthat/test-utils-projects.R b/tests/testthat/test-utils-projects.R index 0bed62d2..6d8584c7 100644 --- a/tests/testthat/test-utils-projects.R +++ b/tests/testthat/test-utils-projects.R @@ -22,7 +22,7 @@ test_that("project_path() uses Quarto environment variables", { temp_dir, "project" )), - QUARTO_PROJECT_DIR = "" + QUARTO_PROJECT_DIR = NA ), expect_identical( project_path("data", "file.csv"), @@ -31,7 +31,7 @@ test_that("project_path() uses Quarto environment variables", { ) withr::with_envvar( c( - QUARTO_PROJECT_ROOT = "", + QUARTO_PROJECT_ROOT = NA, QUARTO_PROJECT_DIR = xfun::normalize_path(file.path(temp_dir, "project")) ), { @@ -134,8 +134,8 @@ test_that("project_path() handles xfun::proj_root() errors gracefully", { test_that("is_running_quarto_project() detects environment variables", { withr::with_envvar( c( - QUARTO_PROJECT_ROOT = "", - QUARTO_PROJECT_DIR = "" + QUARTO_PROJECT_ROOT = NA, + QUARTO_PROJECT_DIR = NA ), expect_null(get_running_project_root()) ) @@ -143,14 +143,14 @@ test_that("is_running_quarto_project() detects environment variables", { withr::with_envvar( c( QUARTO_PROJECT_ROOT = "/some/path", - QUARTO_PROJECT_DIR = "" + QUARTO_PROJECT_DIR = NA ), expect_identical(get_running_project_root(), "/some/path") ) withr::with_envvar( c( - QUARTO_PROJECT_ROOT = "", + QUARTO_PROJECT_ROOT = NA, QUARTO_PROJECT_DIR = "/some/path" ), expect_identical(get_running_project_root(), "/some/path") From 0fcf8dcab2d4ab4253883e8f3b89bd8fa9ce413f Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 14:50:48 +0200 Subject: [PATCH 15/24] Fix example --- R/utils-projects.R | 22 ++++++++++++---------- man/find_project_root.Rd | 20 +++++++++++--------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/R/utils-projects.R b/R/utils-projects.R index 5cec2e37..67ee11da 100644 --- a/R/utils-projects.R +++ b/R/utils-projects.R @@ -213,18 +213,20 @@ get_running_project_root <- function() { #' @return Character Path of the project root directory if found, or `NULL` #' #' @examplesIf quarto_available() -#' dir <- tempfile() -#' dir.create(dir) -#' find_project_root(dir) -#' quarto_create_project(dir) -#' find_project_root(dir) -#' -#' xfun::in_dir(dir, -#' # Check if current directory is in a Quarto project +#' tmpdir <- tempfile() +#' dir.create(tmpdir) +#' find_project_root(tmpdir) +#' quarto_create_project("test-proj", type = "blog", dir = tmpdir, no_prompt = TRUE, quiet = TRUE) +#' blog_post_dir <- file.path(tmpdir, "test-proj", "posts", "welcome") +#' find_project_root(blog_post_dir) +#' +#' xfun::in_dir(blog_post_dir, { +#' # Check if current directory is a Quarto project or in one #' !is.null(find_project_root()) -#' ) +#' }) +#' #' # clean up -#' unlink(dir, recursive = TRUE) +#' unlink(tmpdir, recursive = TRUE) #' #' #' @seealso [get_running_project_root()] for detecting active Quarto rendering diff --git a/man/find_project_root.Rd b/man/find_project_root.Rd index 3504605b..606c13ae 100644 --- a/man/find_project_root.Rd +++ b/man/find_project_root.Rd @@ -21,18 +21,20 @@ interactive sessions. } \examples{ \dontshow{if (quarto_available()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} -dir <- tempfile() -dir.create(dir) -find_project_root(dir) -quarto_create_project(dir) -find_project_root(dir) +tmpdir <- tempfile() +dir.create(tmpdir) +find_project_root(tmpdir) +quarto_create_project("test-proj", type = "blog", dir = tmpdir, no_prompt = TRUE, quiet = TRUE) +blog_post_dir <- file.path(tmpdir, "test-proj", "posts", "welcome") +find_project_root(blog_post_dir) -xfun::in_dir(dir, - # Check if current directory is in a Quarto project +xfun::in_dir(blog_post_dir, { + # Check if current directory is a Quarto project or in one !is.null(find_project_root()) -) +}) + # clean up -unlink(dir, recursive = TRUE) +unlink(tmpdir, recursive = TRUE) \dontshow{\}) # examplesIf} } From df5a08e7af6fad8103f23fa9c139af1cf0171203 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 14:58:50 +0200 Subject: [PATCH 16/24] Fix function name --- R/utils-projects.R | 2 +- tests/testthat/test-utils-projects.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/utils-projects.R b/R/utils-projects.R index 67ee11da..a4814d98 100644 --- a/R/utils-projects.R +++ b/R/utils-projects.R @@ -204,7 +204,7 @@ get_running_project_root <- function() { #' @description #' This function checks if the current working directory is within a Quarto #' project by looking for Quarto project files (`_quarto.yml` or `_quarto.yaml`). -#' Unlike [is_running_quarto_project()], this works both during rendering and +#' Unlike [get_running_project_root()], this works both during rendering and #' interactive sessions. #' #' @param path Character. Path to check for Quarto project files. Defaults to diff --git a/tests/testthat/test-utils-projects.R b/tests/testthat/test-utils-projects.R index 6d8584c7..2bb01cbe 100644 --- a/tests/testthat/test-utils-projects.R +++ b/tests/testthat/test-utils-projects.R @@ -131,7 +131,7 @@ test_that("project_path() handles xfun::proj_root() errors gracefully", { ) }) -test_that("is_running_quarto_project() detects environment variables", { +test_that("get_running_project_root() detects environment variables", { withr::with_envvar( c( QUARTO_PROJECT_ROOT = NA, From 41e4e7ea1b06c590c5e4adf283e990f82336c820 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 4 Jul 2025 15:01:19 +0200 Subject: [PATCH 17/24] Fix help page --- R/utils-projects.R | 5 ++++- man/find_project_root.Rd | 2 +- man/project_path.Rd | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/R/utils-projects.R b/R/utils-projects.R index a4814d98..a5d49df9 100644 --- a/R/utils-projects.R +++ b/R/utils-projects.R @@ -53,7 +53,10 @@ #' # Create a dummy Quarto project structure for example #' tmpdir <- tempfile("quarto_project") #' dir.create(tmpdir) -#' quarto::quarto_create_project('test project', type = 'blog', dir = tmpdir, no_prompt = TRUE, quiet = TRUE) +#' quarto::quarto_create_project( +#' 'test project', type = 'blog', +#' dir = tmpdir, no_prompt = TRUE, quiet = TRUE +#' ) #' project_dir <- file.path(tmpdir, "test project") #' #' # Simulate working within a blog post diff --git a/man/find_project_root.Rd b/man/find_project_root.Rd index 606c13ae..d5df6e1c 100644 --- a/man/find_project_root.Rd +++ b/man/find_project_root.Rd @@ -16,7 +16,7 @@ Character Path of the project root directory if found, or \code{NULL} \description{ This function checks if the current working directory is within a Quarto project by looking for Quarto project files (\verb{_quarto.yml} or \verb{_quarto.yaml}). -Unlike \code{\link[=is_running_quarto_project]{is_running_quarto_project()}}, this works both during rendering and +Unlike \code{\link[=get_running_project_root]{get_running_project_root()}}, this works both during rendering and interactive sessions. } \examples{ diff --git a/man/project_path.Rd b/man/project_path.Rd index 2384c678..c6376bb7 100644 --- a/man/project_path.Rd +++ b/man/project_path.Rd @@ -68,7 +68,10 @@ use a path relative to the project root dynamically during rendering. # Create a dummy Quarto project structure for example tmpdir <- tempfile("quarto_project") dir.create(tmpdir) -quarto::quarto_create_project('test project', type = 'blog', dir = tmpdir, no_prompt = TRUE, quiet = TRUE) +quarto::quarto_create_project( + 'test project', type = 'blog', + dir = tmpdir, no_prompt = TRUE, quiet = TRUE +) project_dir <- file.path(tmpdir, "test project") # Simulate working within a blog post From ddc6ef97f15549be219cf465329ec412d964830f Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 7 Jul 2025 11:29:06 +0200 Subject: [PATCH 18/24] try fix mac os test by changing expected value to computed relative path as when getwd() and tmpdir() are on different drive, this could lead to issues --- tests/testthat/test-utils-projects.R | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-utils-projects.R b/tests/testthat/test-utils-projects.R index 2bb01cbe..10e729bf 100644 --- a/tests/testthat/test-utils-projects.R +++ b/tests/testthat/test-utils-projects.R @@ -3,11 +3,14 @@ test_that("project_path() works with explicit root", { withr::local_dir(temp_dir) expect_identical( project_path("data", "file.csv", root = temp_dir), - file.path("data", "file.csv") + xfun::relative_path(file.path("data", "file.csv"), dir = temp_dir) ) expect_identical( project_path("outputs", "figures", "plot.png", root = temp_dir), - file.path("outputs", "figures", "plot.png") + xfun::relative_path( + file.path("outputs", "figures", "plot.png"), + dir = temp_dir + ) ) expect_identical(project_path(root = temp_dir), ".") }) From 3857c6a8c89b5993ef177d39888d4d86eb89e988 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 7 Jul 2025 11:47:08 +0200 Subject: [PATCH 19/24] Try debug Mac OS build --- .github/workflows/R-CMD-check.yaml | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index a045f7ff..415a49b2 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -26,20 +26,20 @@ jobs: matrix: config: - {os: macos-latest, r: 'release'} - - {os: macos-latest, r: 'release', quarto: 'pre-release'} + # - {os: macos-latest, r: 'release', quarto: 'pre-release'} - - {os: windows-latest, r: 'release'} - - {os: windows-latest, r: 'release', quarto: 'pre-release'} - # use 4.1 to check with rtools40's older compiler - - {os: windows-latest, r: '4.1'} + # - {os: windows-latest, r: 'release'} + # - {os: windows-latest, r: 'release', quarto: 'pre-release'} + # # use 4.1 to check with rtools40's older compiler + # - {os: windows-latest, r: '4.1'} - - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} - - {os: ubuntu-latest, r: 'release'} - - {os: ubuntu-latest, r: 'release', quarto: 'pre-release'} - - {os: ubuntu-latest, r: 'oldrel-1'} - - {os: ubuntu-latest, r: 'oldrel-2'} - - {os: ubuntu-latest, r: 'oldrel-3'} - - {os: ubuntu-latest, r: 'oldrel-4'} + # - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + # - {os: ubuntu-latest, r: 'release'} + # - {os: ubuntu-latest, r: 'release', quarto: 'pre-release'} + # - {os: ubuntu-latest, r: 'oldrel-1'} + # - {os: ubuntu-latest, r: 'oldrel-2'} + # - {os: ubuntu-latest, r: 'oldrel-3'} + # - {os: ubuntu-latest, r: 'oldrel-4'} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} @@ -71,6 +71,9 @@ jobs: extra-packages: any::rcmdcheck, local::. needs: check + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + - uses: r-lib/actions/check-r-package@v2 with: upload-snapshots: true From 46b3bd8d7395ea609f74e5e5862dd902309d0c78 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 7 Jul 2025 11:59:45 +0200 Subject: [PATCH 20/24] Root needs to be normalized before processing --- R/utils-projects.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/utils-projects.R b/R/utils-projects.R index a5d49df9..b02b9df7 100644 --- a/R/utils-projects.R +++ b/R/utils-projects.R @@ -140,6 +140,8 @@ project_path <- function(..., root = NULL) { } } + # Normalize the root path + root <- xfun::normalize_path(root, mustWork = FALSE) # Use xfun::from_root for better path handling path <- rlang::try_fetch( xfun::from_root(..., root = root, error = TRUE), From ff8ad9a69c3a2a737c37d7d4f889bae7410ae20c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 7 Jul 2025 12:00:11 +0200 Subject: [PATCH 21/24] Revert "try fix mac os test by changing expected value to computed relative path" This reverts commit 183effb677f5906949349ca17a366cb045135ff7. --- tests/testthat/test-utils-projects.R | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/testthat/test-utils-projects.R b/tests/testthat/test-utils-projects.R index 10e729bf..2bb01cbe 100644 --- a/tests/testthat/test-utils-projects.R +++ b/tests/testthat/test-utils-projects.R @@ -3,14 +3,11 @@ test_that("project_path() works with explicit root", { withr::local_dir(temp_dir) expect_identical( project_path("data", "file.csv", root = temp_dir), - xfun::relative_path(file.path("data", "file.csv"), dir = temp_dir) + file.path("data", "file.csv") ) expect_identical( project_path("outputs", "figures", "plot.png", root = temp_dir), - xfun::relative_path( - file.path("outputs", "figures", "plot.png"), - dir = temp_dir - ) + file.path("outputs", "figures", "plot.png") ) expect_identical(project_path(root = temp_dir), ".") }) From 44337b15c3cbbc08a1881a3bb230bbc4bc06613f Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 7 Jul 2025 12:00:16 +0200 Subject: [PATCH 22/24] Revert "Try debug Mac OS build" This reverts commit 633a4b20bc38e51ca8e2c62b99746f4b0d65d1a4. --- .github/workflows/R-CMD-check.yaml | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 415a49b2..a045f7ff 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -26,20 +26,20 @@ jobs: matrix: config: - {os: macos-latest, r: 'release'} - # - {os: macos-latest, r: 'release', quarto: 'pre-release'} + - {os: macos-latest, r: 'release', quarto: 'pre-release'} - # - {os: windows-latest, r: 'release'} - # - {os: windows-latest, r: 'release', quarto: 'pre-release'} - # # use 4.1 to check with rtools40's older compiler - # - {os: windows-latest, r: '4.1'} + - {os: windows-latest, r: 'release'} + - {os: windows-latest, r: 'release', quarto: 'pre-release'} + # use 4.1 to check with rtools40's older compiler + - {os: windows-latest, r: '4.1'} - # - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} - # - {os: ubuntu-latest, r: 'release'} - # - {os: ubuntu-latest, r: 'release', quarto: 'pre-release'} - # - {os: ubuntu-latest, r: 'oldrel-1'} - # - {os: ubuntu-latest, r: 'oldrel-2'} - # - {os: ubuntu-latest, r: 'oldrel-3'} - # - {os: ubuntu-latest, r: 'oldrel-4'} + - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'release'} + - {os: ubuntu-latest, r: 'release', quarto: 'pre-release'} + - {os: ubuntu-latest, r: 'oldrel-1'} + - {os: ubuntu-latest, r: 'oldrel-2'} + - {os: ubuntu-latest, r: 'oldrel-3'} + - {os: ubuntu-latest, r: 'oldrel-4'} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} @@ -71,9 +71,6 @@ jobs: extra-packages: any::rcmdcheck, local::. needs: check - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - - uses: r-lib/actions/check-r-package@v2 with: upload-snapshots: true From 872bc1f1ea9ebdd2f89067d2382d3ce902b02fec Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 7 Jul 2025 12:27:14 +0200 Subject: [PATCH 23/24] use xfun::normalize_path everywhere --- NAMESPACE | 1 + R/quarto-package.R | 1 + R/quarto.R | 10 +++++----- R/utils-projects.R | 2 +- tests/testthat/helper.R | 2 +- tests/testthat/test-quarto.R | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index f53d9d96..d96ee908 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -62,5 +62,6 @@ importFrom(tools,vignetteEngine) importFrom(utils,browseURL) importFrom(xfun,base64_encode) importFrom(xfun,env_option) +importFrom(xfun,normalize_path) importFrom(yaml,as.yaml) importFrom(yaml,write_yaml) diff --git a/R/quarto-package.R b/R/quarto-package.R index 2b33d307..bcf487c8 100644 --- a/R/quarto-package.R +++ b/R/quarto-package.R @@ -11,5 +11,6 @@ #' @importFrom tools vignetteEngine #' @importFrom xfun base64_encode #' @importFrom xfun env_option +#' @importFrom xfun normalize_path ## usethis namespace: end NULL diff --git a/R/quarto.R b/R/quarto.R index a4ee8816..c0e7b8f5 100644 --- a/R/quarto.R +++ b/R/quarto.R @@ -21,7 +21,7 @@ quarto_path <- function(normalize = TRUE) { if (!normalize) { return(quarto_path) } - normalizePath(quarto_path, winslash = "/", mustWork = FALSE) + xfun::normalize_path(quarto_path) } get_quarto_path_env <- function() { @@ -257,7 +257,7 @@ quarto_binary_sitrep <- function(verbose = TRUE, debug = FALSE) { return(FALSE) } - quarto_found <- normalizePath(quarto_found, mustWork = FALSE) + quarto_found <- xfun::normalize_path(quarto_found) same_config <- TRUE if (debug) { @@ -271,8 +271,8 @@ quarto_binary_sitrep <- function(verbose = TRUE, debug = FALSE) { )) } - quarto_r_env <- normalizePath(get_quarto_path_env(), mustWork = FALSE) - quarto_system <- normalizePath(unname(Sys.which("quarto")), mustWork = FALSE) + quarto_r_env <- xfun::normalize_path(get_quarto_path_env()) + quarto_system <- xfun::normalize_path(unname(Sys.which("quarto"))) # quarto R package will use QUARTO_PATH env var with higher priority than latest version on path $PATH # and RStudio IDE does not use this environment variable if (!is.na(quarto_r_env) && identical(quarto_r_env, quarto_found)) { @@ -296,7 +296,7 @@ quarto_binary_sitrep <- function(verbose = TRUE, debug = FALSE) { # RStudio IDE > Render button will use RSTUDIO_QUARTO env var with higher priority than latest version on path $PATH rstudio_env <- Sys.getenv("RSTUDIO_QUARTO", unset = NA) if (!is.na(rstudio_env)) { - rstudio_env <- normalizePath(rstudio_env, mustWork = FALSE) + rstudio_env <- xfun::normalize_path(rstudio_env) if (!identical(rstudio_env, quarto_found)) { same_config <- FALSE if (verbose) { diff --git a/R/utils-projects.R b/R/utils-projects.R index b02b9df7..6449599a 100644 --- a/R/utils-projects.R +++ b/R/utils-projects.R @@ -141,7 +141,7 @@ project_path <- function(..., root = NULL) { } # Normalize the root path - root <- xfun::normalize_path(root, mustWork = FALSE) + root <- xfun::normalize_path(root) # Use xfun::from_root for better path handling path <- rlang::try_fetch( xfun::from_root(..., root = root, error = TRUE), diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index bed71e2c..46bcb090 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -211,7 +211,7 @@ transform_quarto_cli_in_output <- function( if (dir_only) { quarto_found <- dirname(quarto_found) } - quarto_found_normalized <- normalizePath(quarto_found, mustWork = FALSE) + quarto_found_normalized <- xfun::normalize_path(quarto_found) # look for non-normalized path lines <- hide_path(lines, quarto_found) # look for normalized path diff --git a/tests/testthat/test-quarto.R b/tests/testthat/test-quarto.R index a2ad9a88..e4d7aab7 100644 --- a/tests/testthat/test-quarto.R +++ b/tests/testthat/test-quarto.R @@ -64,7 +64,7 @@ test_that("quarto CLI sitrep", { skip_if_no_quarto() skip_on_cran() local_reproducible_output(width = 1000) - dummy_quarto_path <- normalizePath("dummy", mustWork = FALSE) + dummy_quarto_path <- xfun::normalize_path("dummy") withr::with_envvar( list(QUARTO_PATH = dummy_quarto_path, RSTUDIO_QUARTO = NA), expect_snapshot( From 8d712e94a6d6bc831f4d6b4ba1578592a85ccbd7 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 8 Jul 2025 12:47:00 +0200 Subject: [PATCH 24/24] Bump version --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index aef7cefd..0b602a24 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: quarto Title: R Interface to 'Quarto' Markdown Publishing System -Version: 1.4.4.9024 +Version: 1.4.4.9025 Authors@R: c( person("JJ", "Allaire", , "jj@posit.co", role = "aut", comment = c(ORCID = "0000-0003-0174-9868")),