# Load helpers
library(devtools)Loading required package: usethis
library(usethis)A practical, end-to-end cookbook for building robust R packages with devtools, roxygen2, and testthat. Written for day‑to‑day use, from scaffolding to checks and tests.
|>)install.packages(c("devtools", "roxygen2", "usethis", "testthat", "pkgdown"))# Load helpers
library(devtools)Loading required package: usethis
library(usethis)# Creates simutils with DESCRIPTION, NAMESPACE placeholder, R/ etc.
#usethis::create_package("simutils")
# Or from an existing project folder
# usethis::create_package(getwd())usethis::use_git()
usethis::use_readme_rmd() # README.Rmd + badge placeholderssimutils/
DESCRIPTION
NAMESPACE
R/
man/Tip: Keep all your R code inside
R/. Avoid subfolders there. Group related functions in sensible files (e.g.,na.R,sample.R).
Run helpers and then edit as needed.
usethis::use_description(fields = list(
Title = "Simulation Utilities",
Description = "Convenience functions for sampling and NA counting.",
`Authors@R` = 'person(given = "Your", family = "Name", role = c("aut","cre"), email = "you@example.com")',
License = "MIT + file LICENSE"
))
usethis::use_mit_license("Your Name")Key fields you usually set:
Package, Title, Version, DescriptionAuthors@R (with aut, cre roles)License (e.g., MIT)Depends, Imports, Suggests (use helpers below)You generally do not edit NAMESPACE by hand. It’s generated from your roxygen tags in code.
@export marks a function for users.@import, @importFrom declare your dependencies at function level.You’ll generate it with devtools::document().
Create your first R file:
# File: R/na.R
#' Count NAs in a vector
#'
#' Count number of missing values (NA) in a numeric/logical/character vector.
#'
#' @param x A vector.
#' @return Integer count of NAs in `x`.
#' @examples
#' sum_na(c(1, NA, 3))
#' sum_na(airquality$Ozone)
#' @export
sum_na <- function(x) {
sum(is.na(x))
}Add a second exported function that works on data frames/matrices:
# File: R/na_counter.R
#' Column-wise NA counts
#'
#' Returns the number of NAs per column for a data frame or matrix.
#'
#' @param x A data frame or matrix.
#' @return A named integer vector of NA counts per column.
#' @examples
#' na_counter(airquality)
#' @export
na_counter <- function(x) {
if (!is.data.frame(x) && !is.matrix(x)) stop("x must be a data.frame or matrix")
colSums(is.na(x))
}A simple sampler utility with a documented argument:
# File: R/sample_from_data.R
#' Sample rows from a data frame
#'
#' Draw a row-wise sample with optional replacement.
#'
#' @param data A data frame.
#' @param size Number of rows to sample.
#' @param replace Logical; sample with replacement?
#' @return A data frame containing the sampled rows.
#' @examples
#' sample_from_data(airquality, 10)
#' sample_from_data(airquality, 10, replace = TRUE)
#' @export
sample_from_data <- function(data, size, replace = FALSE) {
stopifnot(is.data.frame(data))
idx <- sample.int(nrow(data), size, replace = replace)
data[idx, , drop = FALSE]
}# Parses roxygen headers -> creates/updates NAMESPACE and man/*.Rd
devtools::document()This will populate man/ with help files and refresh NAMESPACE.
# Create example data
sim_dat <- data.frame(
ID = 1:10,
Value = sample(1:11, 10),
Apples = sample(c(TRUE, FALSE), 10, replace = TRUE)
)
usethis::use_data(sim_dat, overwrite = TRUE)usethis::use_vignette("getting-started")
# Edit vignettes/getting-started.Rmd, then:
devtools::build_vignettes()usethis::use_testthat()
# Creates tests/testthat.R and tests/testthat/ directory
usethis::use_test("na_counter")
# -> tests/testthat/test-na_counter.R (edit and add tests)Example tests:
# File: tests/testthat/test-na_counter.R
library(testthat)
test_that("na_counter counts NAs correctly for data.frame and matrix", {
test_matrix <- matrix(c(NA, 1, 4, NA, 5, 6), nrow = 2)
air_expected <- c(Ozone = 37, Solar.R = 7, Wind = 0, Temp = 0, Month = 0, Day = 0)
mat_expected <- c(V1 = 1, V2 = 1, V3 = 0)
expect_equal(na_counter(airquality), air_expected)
expect_equal(na_counter(test_matrix), mat_expected)
})
# Non-exported functions can be called with ::: if truly needed
# expect_equal(simutils:::sum_na(airquality$Ozone), 37)Common expectations:
expect_identical(), expect_equal(tolerance = ...), expect_equivalent()expect_error(), expect_warning()expect_output() / expect_output_file() for console textRun the test suite:
devtools::test()Use helpers to declare dependencies in DESCRIPTION (rather than editing by hand):
usethis::use_package("dplyr") # add to Imports
usethis::use_package("ggplot2", type = "Suggests")Guidelines:
# Full check like CRAN does; read 00check.log for details
rc <- devtools::check()
rcTypical findings & quick fixes:
License) → add via use_mit_license() or set in DESCRIPTION.@param and usage matches.airquality, not airuality).devtools::test(); update your code or expectations.# Build source tarball
devtools::build()
# Install the package from source
devtools::install()
# Try it out
#library(simutils)
na_counter(airquality)You can wire up CI so checks run automatically on each commit. (Any provider works; pick what your org supports.)
# Example: set up a CI service config (adjust per provider)
# usethis::use_github_action_check_standard()
# or legacy: usethis::use_travis()usethis::use_pkgdown()
pkgdown::build_site()DESCRIPTION: metadata & dependenciesNAMESPACE: auto‑generated exports/importsR/: function source filesman/: help files (.Rd) generated by roxygen2tests/: unit tests (testthat)vignettes/: long‑form docsdata/ & R/sysdata.rda: package data# 1) Missing License in check
usethis::use_mit_license("Your Name")
# 2) Undocumented argument in check
# -> Make sure @param includes it and re-run document()
devtools::document(); devtools::check()
# 3) Examples fail in check (typo)
# -> Fix the example code, rerun check
# 4) pdflatex not found during check
# -> Install a LaTeX distribution or run check without manual building
# 5) Dependency not available
# -> Ensure it’s in Imports/Suggests and installed in your environmentTurn this file into your team’s template. Duplicate it, search/replace
simutils, and you’re ready to scaffold a new package in minutes.