Themes are functions
A prefab theme is a function that returns a prefab_theme
object – an ordered list of steps. Calling a theme shows you an outline
of the steps; nothing happens to the file system until you pass the
theme to use_theme() or create_project().
library(prefab)
# Calling r_analysis() returns a theme object -- it does not write any files
r_analysis()
#> <theme> 3 steps
#> * file → main.R (skip)
#> * file → README.md (skip)
#> * text → .gitignore (union)
#> ℹ Apply with `use_theme()` or `create_project()`Because themes are functions, they can take parameters, compose with
+, and ship in packages.
Applying themes
Pass a theme to use_theme() (existing project) or
create_project() (new directory):
use_theme(r_analysis())
create_project("~/projects/my-targets-project", r_targets())use_theme() discovers the project root automatically
(via .here, .Rproj, .git, etc.).
create_project() creates the directory first, then applies
the theme.
Composing themes with +
Themes compose with +, concatenating their step
lists:
# Project structure + Claude Code agent config
use_theme(r_analysis() + claude_r_analysis())
create_project("~/projects/new-analysis", r_targets() + claude_r_targets())Order matters: later steps can override earlier ones when the merge
strategy allows it (e.g., two themes deploying the same file with
"overwrite").
Writing your own theme
A theme function is any R function that returns a
prefab_theme via new_theme(). The three step
types are:
-
step_file(source, dest)– deploy a file -
step_text(content, dest)– deploy inline text -
step_run(fn, ...)– execute a function for its side effects
my_analysis <- function(use_data_dir = TRUE, extra_ignores = character(0)) {
from_templates <- from_dir("~/my-templates")
ignore_lines <- c(".Rproj.user", ".Rhistory", ".RData", extra_ignores)
new_theme(
from_templates("main.R", "main.R", strategy = "skip"),
from_templates("README.md", "README.md", strategy = "skip"),
step_text(ignore_lines, ".gitignore", strategy = "union"),
if (use_data_dir) step_run(fs::dir_create, "data", .label = "fs::dir_create")
)
}
use_theme(my_analysis(extra_ignores = "*.csv") + claude_r_analysis())Because themes are just functions, parameters give you conditional
behavior for free. NULL arguments to
new_theme() are silently dropped, so
if (cond) step(...) works naturally.
Source helpers
from_dir() and from_package() create
step-builders that resolve source paths relative to a directory or an
installed R package:
# Resolve from a local directory
from_templates <- from_dir("~/my-templates")
from_templates("header.md", "README.md", strategy = "skip")
# Resolve from an installed package's inst/ directory
from_prefab <- from_package("prefab")
from_prefab("r_analysis/main.R", "main.R", strategy = "skip")from_dir() resolves its path to absolute at creation
time, so later working directory changes do not affect it.
from_package() works with both installed packages and
devtools::load_all().
Distributing themes via a package
Put template files in a package’s inst/ directory and
use from_package():
# In mythemes/R/themes.R
#' @export
my_org_analysis <- function() {
f <- from_package("mythemes")
new_theme(
f("templates/main.R", "main.R", strategy = "skip"),
f("templates/README.md", "README.md", strategy = "skip", data = list()),
f("claude/settings.json", ".claude/settings.json", strategy = "merge_json"),
f("claude/rules/conventions.md", ".claude/rules/conventions.md")
)
}Users compose your themes with any others:
use_theme(mythemes::my_org_analysis() + prefab::claude_r_analysis())Inspecting themes with theme_code()
theme_code() prints R code that reproduces a theme –
useful for understanding built-in themes or as a starting point for
customization:
theme_code(claude_r_analysis())
#> new_theme(
#> from_package("prefab")("claude/settings.json", ".claude/settings.json", strategy = "merge_json"),
#> from_package("prefab")("claude/rules/r_analysis.md", ".claude/rules/r_analysis.md"),
#> step_text(c(".Rproj.user", ".Rhistory", ".RData", ".DS_Store"), ".gitignore", strategy = "union")
#> )Copy the output, edit it into your own theme function, and swap out or add steps. The code is also returned invisibly as a string.
Merge strategies
Every file step declares a strategy for handling
pre-existing destination files. Strategies are what make
use_theme() safe to re-run.
| Strategy | Behavior | Idempotent | Typical use |
|---|---|---|---|
"overwrite" |
Replace the file entirely | Yes | Managed config files |
"skip" |
Do nothing if file exists | Yes | Starter files users will edit |
"union" |
Append lines not already present | Yes |
.gitignore, .Rbuildignore
|
"append" |
Append all content unconditionally | No | Rare; prefer "union"
|
"merge_json" |
Recursively merge JSON objects | Yes | .claude/settings.json |
Guidelines for choosing:
- Files the user should never edit:
"overwrite". - Files the user will customize after first deploy:
"skip". - Line-based config where entries accumulate:
"union". - Structured JSON config:
"merge_json"(objects merge key-by-key, arrays are union-merged, scalar collisions preserve the destination value). - Avoid
"append"unless you want duplicate content on re-runs.
Template rendering
File steps support {{var}} interpolation when
data is non-NULL:
from_templates <- from_dir("~/my-templates")
new_theme(
# data = list() enables rendering with auto-discovered variables only
from_templates("README.md", "README.md", strategy = "skip", data = list()),
# Explicit variables supplement or override auto-context
from_templates("CITATION.md", "CITATION.md",
data = list(org_name = "Acme Corp"))
)Auto-discovered variables (built once per use_theme()
call):
| Variable | Source |
|---|---|
project_dir |
Name of the project root directory |
package_name |
Package field from DESCRIPTION, or
project_dir
|
year |
Current year |
date |
Current date (YYYY-MM-DD) |
A template file like:
# {{project_dir}}
Created {{date}} by {{org_name}}.
is rendered by merging explicit data on top of
auto-context. Explicit values win on collision. If a variable is
referenced but not available, rendering fails with an informative
error.
step_text() does not support data – inline
content can interpolate R variables directly.
