Developing R Packages
Overview
Codified workflow for R package development following the R Packages book (Wickham & Bryan, 2e) and conventions established in BadranSeq. Covers the edit-document-check-commit cycle, dependency management, versioning, and release workflow.
Development Cycle
Every code change follows this sequence before committing:
Edit R/ files → devtools::document() → devtools::check() → git add → git commit
document() regenerates NAMESPACE and man/*.Rd files from roxygen2 comments
check() runs R CMD check locally — expect 0 errors, 0 warnings
- Known NOTEs (license format, timestamp) are acceptable
- Never commit if check fails — fix first, then commit
NAMESPACE & roxygen2
- NAMESPACE is auto-generated by roxygen2 — never edit it manually
- Use
@export to make functions available to users
- Use
@description, @param, @return, @examples for documentation
- Run
devtools::document() after any roxygen comment change
Dependencies
Imports vs Suggests
| Field |
Purpose |
Access pattern |
Imports |
Required at runtime, auto-installed |
pkg::fn() in source code |
Suggests |
Optional (testing, extended features) |
pkg::fn() — never @importFrom |
Depends |
Only for R version floor |
R (>= x.y) |
Rules
- Always use
pkg::fn() syntax for external functions — no exceptions
- Only use
@importFrom for operators (e.g., %>%) that can't use ::
- Add dependencies via
usethis::use_package("pkg") (defaults to Imports)
- For Suggests:
usethis::use_package("pkg", type = "Suggests")
- Never call
library() or require() inside package code
Versioning
Format: <major>.<minor>.<patch> — always . separator, always 3 components.
| Component |
When to bump |
Example |
| Major |
Breaking changes to existing API |
1.0.0 → 2.0.0 |
| Minor |
New exported functions (backward-compatible) |
1.3.1 → 1.4.0 |
| Patch |
Bug fixes, adjustments to existing functions |
1.3.1 → 1.3.2 |
After bumping: update DESCRIPTION, README.Rmd install instructions, re-knit README.md.
Release Workflow
1. Bump version in DESCRIPTION
2. Update README.Rmd version references
3. devtools::build_readme()
4. devtools::check() # must pass
5. git commit + push
6. git tag -a vX.Y.Z -m "BadranSeq X.Y.Z"
7. git push --tags
8. gh release create vX.Y.Z # with release notes
Data in Packages
- User-facing data:
usethis::use_data(obj) → saved to data/obj.rda
- Document datasets in
R/data.R using roxygen2 (@format, @source) — never @export
- Preparation scripts go in
data-raw/
LazyData: true in DESCRIPTION enables lazy loading
Testing
- Initialize with
usethis::use_testthat(3) (edition 3)
- Match test files to source:
R/foo.R → tests/testthat/test-foo.R
- Create tests with
usethis::use_test("foo")
- Run with
devtools::test() or Ctrl+Shift+T
Lifecycle Badges
- Set up with
usethis::use_lifecycle() (one-time)
- Badge in
@description: `r lifecycle::badge("stable")`
- Only badge functions whose stage differs from the package's stage
- Stages: experimental → stable → deprecated/superseded
pkgdown Site
- Never build locally — CI handles it via GitHub Actions after push
- Fix source files (roxygen, vignettes) and push; CI does the rest
CI (GitHub Actions)
- R CMD check runs on push to main via
.github/workflows/R-CMD-check.yaml
- Standard matrix: macOS, Windows, Ubuntu release, Ubuntu devel
- Drop
oldrel-1 if upstream deps require current R
README
README.Rmd is the source — contains live R code chunks
README.md is the rendered output — never edit directly
- Re-knit with
devtools::build_readme() after changes
Common Mistakes
| Mistake |
Fix |
| Editing NAMESPACE manually |
Let roxygen2 generate it via document() |
@importFrom for Suggests packages |
Use pkg::fn() instead |
Committing without check() |
Always document → check → commit |
library() inside package code |
Use pkg::fn() or @importFrom |
| Editing README.md directly |
Edit README.Rmd, then build_readme() |
| Building pkgdown locally |
Let CI handle it |
Version as 1.3 instead of 1.3.0 |
Always use 3 components |
1---2name: developing-r-packages3description: Use when creating, modifying, or maintaining R packages. Use when editing R/ source files, DESCRIPTION, NAMESPACE, roxygen2 documentation, tests, or CI workflows in an R package context. Use when adding functions, bumping versions, managing dependencies, or preparing releases.4---56# Developing R Packages78## Overview910Codified workflow for R package development following the R Packages book (Wickham & Bryan, 2e) and conventions established in BadranSeq. Covers the edit-document-check-commit cycle, dependency management, versioning, and release workflow.1112## Development Cycle1314Every code change follows this sequence **before** committing:1516```17Edit R/ files → devtools::document() → devtools::check() → git add → git commit18```1920- `document()` regenerates NAMESPACE and man/*.Rd files from roxygen2 comments21- `check()` runs R CMD check locally — expect **0 errors, 0 warnings**22- Known NOTEs (license format, timestamp) are acceptable23- Never commit if check fails — fix first, then commit2425## NAMESPACE & roxygen22627- NAMESPACE is **auto-generated by roxygen2** — never edit it manually28- Use `@export` to make functions available to users29- Use `@description`, `@param`, `@return`, `@examples` for documentation30- Run `devtools::document()` after any roxygen comment change3132## Dependencies3334### Imports vs Suggests3536| Field | Purpose | Access pattern |37|-------|---------|---------------|38| `Imports` | Required at runtime, auto-installed | `pkg::fn()` in source code |39| `Suggests` | Optional (testing, extended features) | `pkg::fn()` — never `@importFrom` |40| `Depends` | Only for R version floor | `R (>= x.y)` |4142### Rules4344- **Always use `pkg::fn()` syntax** for external functions — no exceptions45- Only use `@importFrom` for operators (e.g., `%>%`) that can't use `::`46- Add dependencies via `usethis::use_package("pkg")` (defaults to Imports)47- For Suggests: `usethis::use_package("pkg", type = "Suggests")`48- Never call `library()` or `require()` inside package code4950## Versioning5152Format: `<major>.<minor>.<patch>` — always `.` separator, always 3 components.5354| Component | When to bump | Example |55|-----------|-------------|---------|56| **Major** | Breaking changes to existing API | 1.0.0 → 2.0.0 |57| **Minor** | New exported functions (backward-compatible) | 1.3.1 → 1.4.0 |58| **Patch** | Bug fixes, adjustments to existing functions | 1.3.1 → 1.3.2 |5960After bumping: update DESCRIPTION, README.Rmd install instructions, re-knit README.md.6162## Release Workflow6364```651. Bump version in DESCRIPTION662. Update README.Rmd version references673. devtools::build_readme()684. devtools::check() # must pass695. git commit + push706. git tag -a vX.Y.Z -m "BadranSeq X.Y.Z"717. git push --tags728. gh release create vX.Y.Z # with release notes73```7475## Data in Packages7677- User-facing data: `usethis::use_data(obj)` → saved to `data/obj.rda`78- Document datasets in `R/data.R` using roxygen2 (`@format`, `@source`) — never `@export`79- Preparation scripts go in `data-raw/`80- `LazyData: true` in DESCRIPTION enables lazy loading8182## Testing8384- Initialize with `usethis::use_testthat(3)` (edition 3)85- Match test files to source: `R/foo.R` → `tests/testthat/test-foo.R`86- Create tests with `usethis::use_test("foo")`87- Run with `devtools::test()` or `Ctrl+Shift+T`8889## Lifecycle Badges9091- Set up with `usethis::use_lifecycle()` (one-time)92- Badge in `@description`: `` `r lifecycle::badge("stable")` ``93- Only badge functions whose stage **differs** from the package's stage94- Stages: experimental → stable → deprecated/superseded9596## pkgdown Site9798- **Never build locally** — CI handles it via GitHub Actions after push99- Fix source files (roxygen, vignettes) and push; CI does the rest100101## CI (GitHub Actions)102103- R CMD check runs on push to main via `.github/workflows/R-CMD-check.yaml`104- Standard matrix: macOS, Windows, Ubuntu release, Ubuntu devel105- Drop `oldrel-1` if upstream deps require current R106107## README108109- `README.Rmd` is the source — contains live R code chunks110- `README.md` is the rendered output — never edit directly111- Re-knit with `devtools::build_readme()` after changes112113## Common Mistakes114115| Mistake | Fix |116|---------|-----|117| Editing NAMESPACE manually | Let roxygen2 generate it via `document()` |118| `@importFrom` for Suggests packages | Use `pkg::fn()` instead |119| Committing without `check()` | Always document → check → commit |120| `library()` inside package code | Use `pkg::fn()` or `@importFrom` |121| Editing README.md directly | Edit README.Rmd, then `build_readme()` |122| Building pkgdown locally | Let CI handle it |123| Version as `1.3` instead of `1.3.0` | Always use 3 components |