Skip to content

Commit 79b05ca

Browse files
committed
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
1 parent 61039ae commit 79b05ca

11 files changed

+478
-0
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Imports:
2323
htmltools,
2424
jsonlite,
2525
later,
26+
lifecycle,
2627
processx,
2728
rlang,
2829
rmarkdown,

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
export(add_spin_preamble)
44
export(check_newer_version)
5+
export(is_quarto_project)
6+
export(is_running_quarto_project)
57
export(is_using_quarto)
68
export(new_blog_post)
9+
export(project_path)
710
export(quarto_add_extension)
811
export(quarto_available)
912
export(quarto_binary_sitrep)
@@ -46,6 +49,7 @@ importFrom(htmltools,div)
4649
importFrom(htmltools,span)
4750
importFrom(jsonlite,fromJSON)
4851
importFrom(later,later)
52+
importFrom(lifecycle,deprecated)
4953
importFrom(processx,process)
5054
importFrom(processx,run)
5155
importFrom(rlang,caller_env)

R/quarto-package.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#' @importFrom cli cli_inform
77
#' @importFrom htmltools div
88
#' @importFrom htmltools span
9+
#' @importFrom lifecycle deprecated
910
#' @importFrom rlang caller_env
1011
#' @importFrom tools vignetteEngine
1112
#' @importFrom xfun base64_encode

R/utils-projects.R

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#' Get path relative to project root (Quarto-aware)
2+
#'
3+
#' @description
4+
#' `r lifecycle::badge("experimental")`
5+
#'
6+
#' This function constructs file paths relative to the project root when
7+
#' running in a Quarto context (using `QUARTO_PROJECT_ROOT` or `QUARTO_PROJECT_DIR`
8+
#' environment variables), or falls back to intelligent project root detection
9+
#' when not in a Quarto context.
10+
#'
11+
#' It is experimental and subject to change in future releases. The automatic
12+
#' project root detection may not work reliably in all contexts, especially when
13+
#' projects have complex directory structures or when running in non-standard
14+
#' environments. For a more explicit and potentially more robust approach,
15+
#' consider using [here::i_am()] to declare your project structure,
16+
#' followed by [here::here()] for path construction. See examples for comparison.
17+
#'
18+
#' @details
19+
#' The function uses the following fallback hierarchy to determine the project root:
20+
#'
21+
#' - Quarto environment variables set during Quarto commands (e.g., `quarto render`):
22+
#' - `QUARTO_PROJECT_ROOT` environment variable (set by Quarto commands)
23+
#' - `QUARTO_PROJECT_DIR` environment variable (alternative Quarto variable)
24+
#'
25+
#' - Fallback to intelligent project root detection using [xfun::proj_root()] for interactive sessions:
26+
#' - `_quarto.yml` or `_quarto.yaml` (Quarto project files)
27+
#' - `.vscode` directory (VS Code/Positron workspace)
28+
#' - `DESCRIPTION` file with `Package:` field (R package or Project)
29+
#' - `.Rproj` files with `Version:` field (RStudio projects)
30+
#'
31+
#' Last fallback is the current working directory if no project root can be determined.
32+
#' A warning is issued to alert users that behavior may differ between interactive use and Quarto rendering,
33+
#' as in this case the computed path may be wrong.
34+
#'
35+
#' @param ... Character vectors of path components to be joined
36+
#' @param root Project root directory. If `NULL` (default), automatic detection
37+
#' is used following the hierarchy described above
38+
#' @return A character vector of the normalized file path relative to the project root
39+
#'
40+
#' @examples
41+
#' \dontrun{
42+
#' # Reference a data file from project root
43+
#' data_path <- quarto::project_path("data", "my_data.csv")
44+
#'
45+
#' # Reference a script
46+
#' script_path <- quarto::project_path("R", "analysis.R")
47+
#'
48+
#' # Reference nested directories
49+
#' output_path <- quarto::project_path("outputs", "figures", "plot.png")
50+
#'
51+
#' # Explicitly specify root (overrides automatic detection)
52+
#' custom_path <- quarto::project_path("data", "file.csv", root = "/path/to/project")
53+
#'
54+
#' # Alternative approach using here::i_am() (potentially more robust)
55+
#' # This approach requires you to declare where you are in the project:
56+
#' if (requireNamespace("here", quietly = TRUE)) {
57+
#' # Declare that this document is in the project root or subdirectory
58+
#' here::i_am("analysis.qmd") # If in project root
59+
#' # here::i_am("reports/analysis.qmd") # If in subdirectory
60+
#'
61+
#' # Now here::here() will work reliably from the project root
62+
#' data_path_alt <- here::here("data", "my_data.csv")
63+
#' script_path_alt <- here::here("R", "analysis.R")
64+
#' output_path_alt <- here::here("outputs", "figures", "plot.png")
65+
#' }
66+
#' }
67+
#'
68+
#' @seealso
69+
#' * [here::here()] for a similar function that works with R projects
70+
#' * [is_running_quarto_project()] to check if quarto is running with a project context
71+
#' * [xfun::from_root()] for the underlying path construction
72+
#' * [xfun::proj_root()] for project root detection logic
73+
#'
74+
#' @export
75+
project_path <- function(..., root = NULL) {
76+
if (is.null(root)) {
77+
# Try Quarto project environment variables first
78+
quarto_root <- Sys.getenv(
79+
"QUARTO_PROJECT_ROOT",
80+
Sys.getenv("QUARTO_PROJECT_DIR")
81+
)
82+
83+
if (nzchar(quarto_root)) {
84+
root <- quarto_root
85+
} else {
86+
# Try to find project root using xfun::proj_root() with extended rules
87+
tryCatch(
88+
{
89+
# Create extended rules that include Quarto and VS Code project files
90+
extended_rules <- rbind(
91+
# this should be the same as Quarto environment variables
92+
# which are only set when running Quarto commands
93+
c("_quarto.yml", ""), # Quarto project config
94+
c("_quarto.yaml", ""), # Alternative Quarto config
95+
# This is to provide some better fallback than just the working directory
96+
c(".vscode", ""), # VS Code/Positron workspace
97+
xfun::root_rules # Default rules (DESCRIPTION, .Rproj)
98+
)
99+
100+
proj_root <- xfun::proj_root(rules = extended_rules)
101+
root <- if (!is.null(proj_root)) {
102+
proj_root
103+
} else {
104+
cli::cli_warn(
105+
"Failed to determine project root using {.fun xfun::proj_root}. Using current working directory.",
106+
">" = "This may lead to different behavior interactively vs running Quarto commands."
107+
)
108+
getwd()
109+
}
110+
},
111+
error = function(e) {
112+
# Fall back to working directory if proj_root() fails
113+
cli::cli_warn(c(
114+
"Failed to determine project root: {e$message}. Using current working directory as a fallback.",
115+
">" = "This may lead to different behavior interactively vs running Quarto commands."
116+
))
117+
root <- getwd()
118+
}
119+
)
120+
}
121+
}
122+
123+
# Use xfun::from_root for better path handling
124+
path <- tryCatch(
125+
xfun::from_root(..., root = root, error = FALSE),
126+
error = function(e) file.path(root, ...)
127+
)
128+
path
129+
}
130+
131+
#' Check if running within a Quarto project context
132+
#'
133+
#' @description
134+
#' This function checks if the current R session is running within a Quarto
135+
#' project context by detecting Quarto project environment variables.
136+
#'
137+
#' @details
138+
#' Quarto sets `QUARTO_PROJECT_ROOT` and `QUARTO_PROJECT_DIR` environment
139+
#' variables when executing commands within a Quarto project context (e.g.,
140+
#' `quarto render`, `quarto preview`). This function detects their presence.
141+
#'
142+
#' Note that this function will return `FALSE` when running code interactively
143+
#' in an IDE (even within a Quarto project directory), as these specific
144+
#' environment variables are only set during Quarto command execution.
145+
#'
146+
#' @return Logical indicating if Quarto project environment variables are set
147+
#'
148+
#' @seealso
149+
#' * [is_quarto_project()] for checking Quarto project structure
150+
#' * [project_path()] for constructing paths relative to the project root
151+
#' @examples
152+
#' \dontrun{
153+
#' # This will be TRUE during `quarto render` in a project
154+
#' is_running_quarto_project()
155+
#'
156+
#' # This will be FALSE when not running during `quarto_render` (e.g. interactively)
157+
#' is_running_quarto_project()
158+
#' }
159+
#' @export
160+
is_running_quarto_project <- function() {
161+
nzchar(Sys.getenv("QUARTO_PROJECT_ROOT")) ||
162+
nzchar(Sys.getenv("QUARTO_PROJECT_DIR"))
163+
}
164+
165+
#' Check if working within a Quarto project structure
166+
#'
167+
#' @description
168+
#' This function checks if the current working directory is within a Quarto
169+
#' project by looking for Quarto project files (`_quarto.yml` or `_quarto.yaml`).
170+
#' Unlike [is_running_quarto_project()], this works both during rendering and
171+
#' interactive sessions.
172+
#'
173+
#' @param path Character. Path to check for Quarto project files. Defaults to
174+
#' current working directory.
175+
#'
176+
#' @return Logical indicating if a Quarto project structure is detected
177+
#'
178+
#' @examplesIf quarto_available()
179+
#' dir <- tempfile()
180+
#' dir.create(dir)
181+
#' is_quarto_project(dir)
182+
#' quarto_create_project(dir)
183+
#' is_quarto_project(dir)
184+
#'
185+
#' xfun::in_dir(dir,
186+
#' # Check if current directory is in a Quarto project
187+
#' is_quarto_project()
188+
#' )
189+
#' # clean up
190+
#' unlink(dir, recursive = TRUE)
191+
#'
192+
#'
193+
#' @seealso [is_running_quarto_project()] for detecting active Quarto rendering
194+
#' @export
195+
is_quarto_project <- function(path = ".") {
196+
tryCatch(
197+
{
198+
quarto_rules <- rbind(
199+
c("_quarto.yml", ""),
200+
c("_quarto.yaml", "")
201+
)
202+
203+
proj_root <- xfun::proj_root(path = path, rules = quarto_rules)
204+
!is.null(proj_root)
205+
},
206+
error = function(e) {
207+
FALSE
208+
}
209+
)
210+
}
Lines changed: 21 additions & 0 deletions
Loading
Lines changed: 21 additions & 0 deletions
Loading

man/figures/lifecycle-stable.svg

Lines changed: 29 additions & 0 deletions
Loading
Lines changed: 21 additions & 0 deletions
Loading

man/is_quarto_project.Rd

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)