install.packages("golem")
library(golem)
::create_golem(path = "gap") golem
The golem
framework
The golem
package provides many options for R programmers experienced with creating R packages, but who are looking to streamline their application development.1
“opinionated framework for building production-grade Shiny applications”
Getting started
To create a new golem
app from the console, enter the following:
If creating a golem
app from RStudio’s New Project Wizard, the following defaults are available:
Package files
The initial folder structure for a new golem
application is below:
gap
├── DESCRIPTION
├── NAMESPACE
├── R
│ ├── app_config.R
│ ├── app_server.R
│ ├── app_ui.R
│ └── run_app.R
├── dev
│ ├── 01_start.R
│ ├── 02_dev.R
│ ├── 03_deploy.R
│ └── run_dev.R
├── gap.Rproj
├── inst
│ ├── app
│ │ └── www
│ │ └── favicon.ico
│ └── golem-config.yml
├── man
│ └── run_app.Rd
├── renv
│ ├── activate.R
│ ├── settings.json
│ └── staging
└── renv.lock
9 directories, 17 files
The dev/
folder contains golem
‘s ’guided tour’ scripts. These contain functions to help guide application development.
gap/dev/
├── 01_start.R
├── 02_dev.R
├── 03_deploy.R
└── run_dev.R
1 directory, 4 files
- 1
-
dev/01_start.R
opens automatically - 2
-
dev/run_dev.R
is for running the ‘development version’ of the application.
Getting started
If you are familiar with R package development, you can think of the dev/
scripts as a ‘Shiny app-package development checklist.’ dev/01_start.R
contains various usethis
, devtools
, and golem
functions for for creating common package development files:
DESCRIPTION
- In the
dev/01_start.R
script, users build aDESCRIPTION
file withgolem::fill_desc()
fill_desc()
uses the desc
package and the sections are entered in a key = "value"
format
::fill_desc(
golempkg_name = "gap",
pkg_title = "A goelm app-package",
pkg_description = "An example golem app.",
author_first_name = "Martin",
author_last_name = "Frigaard",
author_email = "mjfrigaard@pm.me",
repo_url = NULL
)
Development dependencies
golem::install_dev_deps()
installs the packages you’ll need to develop yourgolem
app.
Common files
LICENSE:
usethis::use_mit_license()
README:
golem::use_readme_rmd(open = FALSE)
- Followed by
devtools::build_readme()
- Followed by
Code of Conduct:
usethis::use_code_of_conduct(contact = "Golem User")
Lifecycle badge:
usethis::use_lifecycle_badge("Experimental")
NEWS.md:
usethis::use_news_md(open = FALSE)
The following files are added to the root golem
app-package folder:
gap/
├── CODE_OF_CONDUCT.md
├── LICENSE
├── LICENSE.md
├── NEWS.md
└── README.Rmd
The next golem
functions in dev/01_start.R
are for setting up the testing infrastructure and recommended tests.
Test suite files
golem::use_recommended_tests()
Adds
testthat
to theSuggests
field inDESCRIPTION
Adds
Config/testthat/edition: 3
toDESCRIPTION
tests/
: Installs spelling
package, includes it in the Suggests
field in DESCRIPTION
, adds the spelling.R
to tests/
, and adds recommended unit tests to tests/testthat
tests/
├── spelling.R
└── testthat
└── test-golem-recommended.R
inst/
: The following words will be added to the WORDLIST
: Lifecycle
, README
, goelm
, golem
inst/
└── WORDLIST
Favicon
golem::use_favicon()
to usegolem
’s favicongolem::remove_favicon()
to removegolem
s favicon.
R/
: Create R/golem_utils_ui.R
and R/golem_utils_server.R
utility functions in the R/
folder
R/
├── golem_utils_server.R
└── golem_utils_ui.R
with_test = TRUE
: creates the associated tests in the tests/testthat
folder.
tests/
└── testthat/
├── test-golem_utils_server.R
└── test-golem_utils_ui.R
Both R/golem_utils_ui.R
and R/golem_utils_server.R
contain a lot of helper functions that come in handy if you’re tired of writing out particular function names (like reactiveValuesToList()
or column(width = 6)
/column(width = 12)
)
Check them out here:
Git
usethis::use_git()
to initiate a Git repo.usethis::use_git_remote()
to configure the remote.
Now that we’ve reached the end of the dev/01_start.R
script, we can open dev/02_dev.R
and begin developing our application.
Development
The dev/02_dev.R
file covers the ‘development’ phase of a new a golem
app-package. Most of the golem
functions in dev/02_dev.R
will create files in the R/
and inst/
folders.
Application code
Before we create any new files, we’re going to dive into the code that’s included in a new golem
app-package. Most of these files are in the R/
folder, but others live in inst/
and dev/
.
UI and Server
The two pre-configured UI and server functions are in R/app_ui.R
and R/app_server.R
:
R/
├── app_config.R
├── app_server.R
├── app_ui.R
└── run_app.R
1 directory, 4 files
app_ui()
and app_server()
are golem
-flavored UI and server files, which means they include the @noRd
tag and include additional golem
utilities.
App UI
<- function(request) {
app_ui tagList(
# Leave this function for adding external
# resources
golem_add_external_resources(),
# Your application UI logic
fluidPage(
# Remove the line below to start
# building your UI
::golem_welcome_page()
golem
)
) }
golem_add_external_resources()
is a wrapper for shiny::addResourcePath()
and htmltools::htmlDependency()
<- function() {
golem_add_external_resources add_resource_path(
"www",
app_sys("app/www")
)
$head(
tagsfavicon(),
bundle_resources(
path = app_sys("app/www"),
app_title = "gap"
)# Add here other external resources
# for example, you can add
# shinyalert::useShinyalert()
) }
App Server
<- function(input, output, session) {
app_server # Your application server logic
}
Configuration
The R/app_config.R
file contains two functions: app_sys()
and get_golem_config()
. If you do some digging, you’ll find these utilities are wrappers for other package development functions:
R/app_config.R
: app_sys()
is a wrapper for system.file()
.
<- function(...) {
app_sys system.file(..., package = "gap")
}
R/app_config.R
: get_golem_config()
is also included in R/app_config.R
, which reads the inst/golem-config.yml
configuration file.
<- function(
get_golem_config
value,config = Sys.getenv(
"GOLEM_CONFIG_ACTIVE",
Sys.getenv(
"R_CONFIG_ACTIVE",
"default"
)
),use_parent = TRUE,
# Modify this if your config file
# is somewhere else
file = app_sys("golem-config.yml")
) {::get(
configvalue = value,
config = config,
file = file,
use_parent = use_parent
) }
golem_config.yml: the inst/golem-config.yml
file gives access to the app version, name, and (development) working directory, so it can be used to add “production-only elements” and is “shareable across golem
projects.”
default:
golem_name: gap
golem_version: 0.0.0.9000
app_prod: no
production:
app_prod: yes
dev:
golem_wd: !expr golem::pkg_path()
Standalone app function
run_app()
is the standalone app function exported from our golem
app-package.
run_app()
is a wrapper around golem::with_golem_options()
and shiny::shinyApp()
.
<- function(
run_app onStart = NULL,
options = list(),
enableBookmarking = NULL,
uiPattern = "/",
...
) {with_golem_options(
app = shinyApp(
ui = app_ui,
server = app_server,
onStart = onStart,
options = options,
enableBookmarking = enableBookmarking,
uiPattern = uiPattern
),golem_opts = list(...)
) }
External resources
The inst/app/
and inst/app/wwww
folders work just like the inst/extdata
folder (they are loaded when the package is installed and makes these files available in the application at runtime).
inst/
└── app/
└── www/
2 directories
Application code recap
The golem
framework provides a structured approach to building production-grade Shiny applications as R packages, promoting modular programming, testing, version control, and deployment readiness for enterprise or long-term projects.
UI & Server
The UI and server functions in R/app_ui.R
and R/app_server.R
are the entry points of the golem application, serving as wrappers that connect the app’s modules and centralize the UI layout and server logic.
Utility functions
golem
’s utility functions from golem::use_utils_ui(with_test = TRUE)
and golem::use_utils_server(with_test = TRUE)
offer boilerplate helper functions and tests for UI and server code.
Tests
The function golem::use_recommended_tests()
sets up scaffolding in tests/testthat/
to ensure your app’s UI, server logic, and utility functions work as intended. It includes examples for testing modules and behaviors, facilitating test-driven or behavior-driven development.
Configurations
The configuration files in golem
apps (R/app_config.R
and inst/golem-config.yml
) manage environment-specific settings like API keys and database credentials. They separate app configurations from code, supporting different deployment environments (dev, staging, production).
Dependencies
dev/02_dev.R
starts by dealing with the package dependencies (i.e., the packages we’ll need for our golem
app to run).
Package dependencies
attachment::att_amend_desc()
captures the dependencies in the R/
folder and includes them in the DESCRIPTION
file.
Writing code
Building a golem
application is similar to developing a regular R package, but some of the development processes have been bundled into wrapper functions. The sections below cover creating modules, utility functions, and tests:
Modules
New modules can be created with golem::add_module()
. I’ve provided an example below to demonstrate the variable arguments and options:
::add_module(name = 'scatter_display', with_test = TRUE, export = TRUE) golem
The code above creates the following files:
R/mod_scatter_display.R
is a boilerplate Shiny module (with UI and server functions):show/hide R/mod_name.R
#' scatter_display UI Function #' #' @description A shiny Module. #' #' @param id,input,output,session Internal parameters for {shiny}. #' #' @rdname mod_scatter_display #' @export #' #' @importFrom shiny NS tagList <- function(id) { mod_scatter_display_ui <- NS(id) ns tagList( ) } #' scatter_display Server Functions #' #' @rdname mod_scatter_display #' @export <- function(id){ mod_scatter_display_server moduleServer(id, function(input, output, session){ <- session$ns ns }) } ## To be copied in the UI # mod_scatter_display_ui("scatter_display_1") ## To be copied in the server # mod_scatter_display_server("scatter_display_1")
tests/testthat/test-mod_scatter_display.R
: is a test file for the module with the following boilerplate tests:show/hide tests/testthat/test-mod_name.R
testServer( mod_scatter_display_server,# Add here your module params args = list() , {<- session$ns ns expect_true( inherits(ns, "function") )expect_true( grepl(id, ns("")) )expect_true( grepl("test", ns("test")) )# Here are some examples of tests you can # run on your module # - Testing the setting of inputs # session$setInputs(x = 1) # expect_true(input$x == 1) # - If ever your input updates a reactiveValues # - Note that this reactiveValues must be passed # - to the testServer function via args = list() # expect_true(r$x == 1) # - Testing output # expect_true(inherits(output$tbl$html, "html")) }) test_that("module ui works", { <- mod_scatter_display_ui(id = "test") ui ::expect_shinytaglist(ui) golem# Check that formals have not been removed <- formals(mod_scatter_display_ui) fmls for (i in c("id")){ expect_true(i %in% names(fmls)) } })
export = TRUE
exports the module functions (@export
) with the name provided toname
(@rdname
).
The golem::add_module()
function was used to add the following modules and their accompanying test files.
R/
├── mod_aes_inputs.R
├── mod_scatter_display.R
└── mod_var_inputs.R
Expand the code below to view the modules:
show/hide mod_aes_inputs.R
#' aes_inputs UI Function
#'
#' @description A shiny Module.
#'
#' @param id,input,output,session Internal parameters for {shiny}.
#'
#' @rdname mod_aes_inputs_ui
#'
#' @importFrom shiny NS tagList
#'
#' @export
#'
<- function(id) {
mod_aes_inputs_ui <- NS(id)
ns tagList(
sliderInput(
inputId = ns("alpha"),
label = "Alpha:",
min = 0, max = 1, step = 0.1,
value = 0.7
),sliderInput(
inputId = ns("size"),
label = "Size:",
min = 0, max = 5,
value = 3
),textInput(
inputId = ns("plot_title"),
label = "Plot title",
placeholder = "Enter plot title"
)
)
}
#' aes_inputs Server Functions
#'
#' @rdname mod_aes_inputs_server
#'
#' @export
#'
<- function(id){
mod_aes_inputs_server moduleServer(id, function(input, output, session){
<- session$ns
ns return(
reactive({
list(
"alpha" = input$alpha,
"size" = input$size,
"plot_title" = input$plot_title
)
})
)
})
}
## To be copied in the UI
# mod_aes_inputs_ui("aes_inputs_1")
## To be copied in the server
# mod_aes_inputs_server("aes_inputs_1")
show/hide mod_var_inputs.R
#' var_inputs UI Function
#'
#' @description A shiny Module.
#'
#' @param id,input,output,session Internal parameters for {shiny}.
#'
#' @rdname mod_var_inputs_ui
#'
#'
#' @importFrom shiny NS tagList
#'
#' @export
#'
<- function(id) {
mod_var_inputs_ui <- NS(id)
ns tagList(
selectInput(
inputId = ns("y"),
label = "Y-axis:",
choices = c(
"IMDB rating" = "imdb_rating",
"IMDB number of votes" = "imdb_num_votes",
"Critics Score" = "critics_score",
"Audience Score" = "audience_score",
"Runtime" = "runtime"
),selected = "audience_score"
),selectInput(
inputId = ns("x"),
label = "X-axis:",
choices = c(
"IMDB rating" = "imdb_rating",
"IMDB number of votes" = "imdb_num_votes",
"Critics Score" = "critics_score",
"Audience Score" = "audience_score",
"Runtime" = "runtime"
),selected = "imdb_rating"
),selectInput(
inputId = ns("z"),
label = "Color by:",
choices = c(
"Title Type" = "title_type",
"Genre" = "genre",
"MPAA Rating" = "mpaa_rating",
"Critics Rating" = "critics_rating",
"Audience Rating" = "audience_rating"
),selected = "mpaa_rating"
)
)
}
#' var_inputs Server Functions
#'
#' @rdname mod_var_inputs_server
#'
#' @export
#'
<- function(id){
mod_var_inputs_server moduleServer(id, function(input, output, session){
<- session$ns
ns return(
reactive({
list(
"y" = input$y,
"x" = input$x,
"z" = input$z
)
})
)
})
}
## To be copied in the UI
# mod_var_inputs_ui("var_inputs_1")
## To be copied in the server
# mod_var_inputs_server("var_inputs_1")
show/hide mod_scatter_display.R
#' scatter_display UI Function
#'
#' @description A shiny Module.
#'
#' @param id,input,output,session Internal parameters for {shiny}.
#'
#' @rdname mod_scatter_display_ui
#'
#' @importFrom shiny NS tagList
#'
#' @export
#'
<- function(id) {
mod_scatter_display_ui <- NS(id)
ns tagList(
$br(),
tagsplotOutput(outputId = ns("scatterplot"))
)
}
#' scatter_display Server Functions
#'
#' @rdname mod_scatter_display_server
#'
#'
#' @export
#'
<- function(id, var_inputs, aes_inputs) {
mod_scatter_display_server moduleServer(id, function(input, output, session){
<- session$ns
ns
# observe({
# browser()
<- reactive({
inputs <- tools::toTitleCase(aes_inputs()$plot_title)
plot_title list(
x = var_inputs()$x,
y = var_inputs()$y,
z = var_inputs()$z,
alpha = aes_inputs()$alpha,
size = aes_inputs()$size,
plot_title = plot_title
)
})
$scatterplot <- renderPlot({
output<- scatter_plot(
plot # data --------------------
df = movies,
x_var = inputs()$x,
y_var = inputs()$y,
col_var = inputs()$z,
alpha_var = inputs()$alpha,
size_var = inputs()$size
)+
plot ::labs(
ggplot2title = inputs()$plot_title,
x = stringr::str_replace_all(tools::toTitleCase(inputs()$x), "_", " "),
y = stringr::str_replace_all(tools::toTitleCase(inputs()$y), "_", " ")
+
) ::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
ggplot2
})
# })
})
}
## To be copied in the UI
# mod_scatter_display_ui("scatter_display_1")
## To be copied in the server
# mod_scatter_display_server("scatter_display_1")
Utility functions
Utility functions can also be created directly with golem::add_utils()
or golem::add_fct()
.2 The scatter_plot()
utility function was created using the code below:
::add_utils(name = "scatter_display", with_test = TRUE) golem
R/utils_scatter_display.R
contains the followingroxygen2
documentation:#' scatter_display #' #' @description A utils function #' #' @return The return value, if any, from executing the utility. #' #' @noRd #'
tests/testthat/test-utils_scatter_display.R
contains the following boilerplate test:test_that("multiplication works", { expect_equal(2 * 2, 4) })
golem::add_utils()
was used to create the following utility functions:
R/
├── utils_gap_theme.R
├── utils_scatter_display.R
└── utils_tests.R
Expand the code below to view the utility functions:
show/hide utils_gap_theme.R
#' thematic golem theme
#'
#' @returns bslib theme
#'
#' @export
#'
<- bslib::bs_theme(
gap_theme bg = "white",
fg = "black",
accent = "#F28E2B",
bootswatch = "united", # optional (choose a bootstrap theme)
primary = "#482878",
secondary = "#F28E2B",
success = "#59A14F",
info = "#4E79A7",
warning = "#F28E2B",
danger = "#E15759",
base_font = bslib::font_google("Ubuntu"),
heading_font = bslib::font_google("Raleway")
)
show/hide utils_scatter_display.R
#' scatter plot utility function
#'
#' @description A utils function
#'
#' @return The return value, if any, from executing the utility.
#'
#' @rdname scatter_plot
#'
#' @importFrom rlang .data
#'
#' @export
#'
<- function(df, x_var, y_var, col_var, alpha_var, size_var) {
scatter_plot ::ggplot(data = df,
ggplot2::aes(x = .data[[x_var]],
ggplot2y = .data[[y_var]],
color = .data[[col_var]])) +
::geom_point(alpha = alpha_var, size = size_var)
ggplot2 }
show/hide utils_tests.R
#' test_logger
#'
#' @description A fct function
#'
#' @param start A character string indicating the start tag. Default is `NULL`.
#' @param end A character string indicating the end tag. Default is `NULL`.
#' @param msg A character string containing the log message.
#'
#' @return The return value, if any, from executing the function.
#'
#' @rdname test_logger
#'
#' @export
#'
<- function(start = NULL, end = NULL, msg) {
test_logger if (is.null(start) & is.null(end)) {
cat("\n")
::cli_inform("TEST:[{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}] = {msg}")
clielse if (!is.null(start) & is.null(end)) {
} cat("\n")
::cli_inform("\nTEST: START [{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}] {start} = {msg}")
clielse if (is.null(start) & !is.null(end)) {
} cat("\n")
::cli_inform("\nTEST: END [{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}] {end} = {msg}")
clielse {
} cat("\n")
::cli_inform("\nTEST: START [{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}]{start} = {msg}")
clicat("\n")
::cli_inform("\nTEST: END [{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}] {end} = {msg}")
cli
} }
UI
golem
UI contents are placed in R/app_ui.R
:
#' The application User-Interface
#'
#' @param request Internal parameter for `{shiny}`.
#' DO NOT REMOVE.
#'
#' @import shiny
#'
#' @noRd
#'
<- function(request) {
app_ui tagList(
# Leave this function for adding external resources
golem_add_external_resources(),
# Your application UI logic
::page_fillable(
bslibtitle = "Movie Reviews (bslib)",
theme = gap_theme,
::layout_sidebar(
bslibsidebar = bslib::sidebar(
mod_var_inputs_ui("vars"),
mod_aes_inputs_ui("aes")
),::card(
bslibfull_screen = TRUE,
::card_header(tags$h4(tags$em("Brought to you by ",
bslib$img(
tagssrc = "www/golem.png",
height = 100,
width = 100,
style = "margin:10px 10px"
)))
),::card_body(
bslibmod_scatter_display_ui("plot")
)
)
)
)
) }
We also need to update the golem_add_external_resources()
function with bslib::bs_theme_dependencies()
and include our theme:
#' Add external Resources to the Application
#'
#' This function is internally used to add external
#' resources inside the Shiny application.
#'
#' @import shiny
#' @importFrom golem add_resource_path activate_js favicon bundle_resources
#' @noRd
<- function() {
golem_add_external_resources add_resource_path(
"www",
app_sys("app/www")
)$head(
tagsfavicon(),
bundle_resources(
path = app_sys("app/www"),
app_title = "gap"
),# Add here other external resources
# for example, you can add shinyalert::useShinyalert()
::bs_theme_dependencies(theme = gap_theme)
bslib
) }
Server
We’ll add the module server functions to R/app_server.R
:
#' The application server-side
#'
#' @param input,output,session Internal parameters for {shiny}.
#' DO NOT REMOVE.
#' @import shiny
#' @noRd
<- function(input, output, session) {
app_server # Your application server logic
<- mod_var_inputs_server("vars")
selected_vars
<- mod_aes_inputs_server("aes")
selected_aes
mod_scatter_display_server("plot",
var_inputs = selected_vars,
aes_inputs = selected_aes)
}
We’ll also include a call to thematic::thematic_shiny()
in the standalone app run_app()
function:
#' Run the Shiny Application
#'
#' @param ... arguments to pass to golem_opts.
#' See `?golem::get_golem_options` for more details.
#' @inheritParams shiny::shinyApp
#'
#' @export
#' @importFrom shiny shinyApp
#' @importFrom golem with_golem_options
<- function(
run_app onStart = NULL,
options = list(),
enableBookmarking = NULL,
uiPattern = "/",
...
) {::thematic_shiny()
thematicwith_golem_options(
app = shinyApp(
ui = app_ui,
server = app_server,
onStart = onStart,
options = options,
enableBookmarking = enableBookmarking,
uiPattern = uiPattern
),golem_opts = list(...)
) }
This ensures the application will use our bslib
theme with thematic.
Writing code recap
golem
development enforces a standardized file structure and workflow, helping maintain consistent development practices. The helper functions encourage breaking the application into testable, reusable modules.
Adding modules
add_module()
adds a .R
file with a mod_
prefix (an optional test can be included with the with_test = TRUE
argument)
Adding utility functions
Utility functions can be added with add_utils()
or add_fct()
(also includes the with_test
option for tests).
Adding non-R files
dev/02_dev.R
includes helpers for adding JavaScript, CSS, and other files to theinst/app/www/
folder:golem::add_js_file()
golem::add_js_handler()
golem::add_css_file()
golem::add_sass_file()
golem::add_any_file()
Data
movies.RData
is added to inst/extdata
and loaded into the package with data-raw/movies.R
:
## code to prepare `movies` dataset goes here
<- system.file('extdata/movies.RData', package = 'gap')
pth load(pth)
::use_data(movies, overwrite = TRUE) usethis
After calling usethis::use_data_raw('movies')
, we use system.file()
to locate the inst/extdata/movies.RData
file with the code in data-raw/movies.R
and save it in the data/
folder.
External data
inst/extdata/
contains the external data files.3
Raw data
data-raw/
contains movies.R
, which is used to load extdata/movies.RData
and create the data/movies.rda
file.
Package data
data/
contains the movies.rda
file used in the application.
inst
└── extdata
└── movies.RData
2 directories, 1 file
data-raw
└── movies.R
1 directory, 1 file
data
└── movies.rda
1 directory, 1 file
Test files
The test suite was set up in the dev/01_start.R
script with use_recommended_tests()
, which is essentially a wrapper for usethis::use_testthat()
with a few extras.4
Unit tests
The with_test = TRUE
argument in add_module()
and add_utils()
ensures the modules and functions also create the accompanying test files:
tests/testthat/
├── test-mod_aes_inputs.R
├── test-mod_scatter_display.R
├── test-mod_var_inputs.R
├── test-utils_scatter_display.R
└── test-utils_tests.R
System tests
System tests can be performed with shinytest2
(similar to non-package or non-golem
apps). I’ve stored two shinytest2
example tests in vignettes/shinytest2.Rmd
(the test-shinytest2
example is below).
test-shinytest2.R
contains the boilerplate test from running shinytest2::record_test()
:
library(shinytest2)
test_that("{shinytest2} recording: gap movies app", {
<- AppDriver$new(
app name = "gap-movies-app",
height = 800,
width = 800,
timeout = 15000,
load_timeout = 15000)
$set_inputs(`vars-y` = "imdb_num_votes")
app$set_inputs(`vars-x` = "critics_score")
app$set_inputs(`vars-z` = "genre")
app$set_inputs(`vars-alpha` = 0.7)
app$set_inputs(`vars-size` = 3)
app$set_inputs(`vars-plot_title` = "New plot title")
app$expect_values()
app })
Test coverage
Code test coverage is handled by usethis::use_coverage()
and covrpage::covrpage()
.5
Code coverage
use_coverage()
requires atype
(“codecov” or “coveralls”).The
covrpage()
package provides a test coverage report intests/README.md
file. This file includes a report of the R files tested, the unit test context, number of tests, and the test status.The
README.Rmd
will include the results fromdevtools::check(quiet = TRUE)
and a detailed report on tests coverage (see below).
The code coverage report provided by the README.Rmd
file is below:
::package_coverage()
covr#> gap Coverage: 87.18%
#> R/run_app.R: 0.00%
#> R/utils_tests.R: 53.85%
#> R/golem_utils_server.R: 77.78%
#> R/mod_scatter_display.R: 78.38%
#> R/golem_utils_ui.R: 87.94%
#> R/app_config.R: 100.00%
#> R/app_server.R: 100.00%
#> R/app_ui.R: 100.00%
#> R/mod_aes_inputs.R: 100.00%
#> R/mod_var_inputs.R: 100.00%
#> R/utils_scatter_display.R: 100.00%
Deploy
The final dev/03_deploy.R
script contains options for pre-deployment checks, using a Dockerfile, and deploying your app to Posit services.
Running checks
devtools::check()
andrhub::check_for_cran()
6 will checks your app-package under the assumption it’s being submitted to CRAN.devtools::build()
will create atar.gz
file (useful for downloads/installs or a package management system).
Posit products
-
All of these functions create an
app.R
file to the root of your app-package.pkgload::load_all()
is called in the newapp.R
file (so this package is added to theImports
field in theDESCRIPTION
).
# Launch the ShinyApp (Do not remove this comment)
# To deploy, run: rsconnect::deployApp()
# Or use the blue button on top of this file
::load_all(export_all = FALSE,
pkgloadhelpers = FALSE,
attach_testthat = FALSE)
options( "golem.app.prod" = TRUE)
::run_app() # add parameters here (if any) gap
R/_disable_autoload.R
These functions also add an empty R/_disable_autoload.R
file to handle the loadSupport()
warning.
Hidden files
Various hidden files are added (.rscignore
) and included in the .Rbuildignore
("^rsconnect$"
to ignore rsconnect
folder, "^app\\.R$"
to ignore the app.R
file, etc.).
Deploy code
dev/03_deploy.R
includes boilerplate code for deploying your application using rsconnect::deployApp()
.
::deployApp(
rsconnectappName = desc::desc_get_field("Package"),
appTitle = desc::desc_get_field("Package"),
appFiles = c(
# Add any additional files unique to your app here.
"R/",
"inst/",
"data/",
"NAMESPACE",
"DESCRIPTION",
"app.R"
),appId = rsconnect::deployments(".")$appID,
lint = FALSE,
forceUpdate = TRUE
)
Docker
There are two golem functions for creating Dockerfile
s:
Below is an example of using add_dockerfile_with_renv()
:
tmp/deploy
folder
deploy/
├── Dockerfile
├── Dockerfile_base
├── README
├── gap_0.0.0.9000.tar.gz
└── renv.lock.prod
Dockerfile_base
Dockerfile_base
is used to create a base image with necessary dependencies and configurations.
show/hide Dockerfile_base
FROM rocker/verse:4.4.2
RUN apt-get update -y && apt-get install -y make zlib1g-dev git libicu-dev && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/
RUN echo "options(renv.config.pak.enabled = FALSE, repos = c(CRAN = 'https://cran.rstudio.com/'), download.file.method = 'libcurl', Ncpus = 4)" | tee /usr/local/lib/R/etc/Rprofile.site | tee /usr/lib/R/etc/Rprofile.site
RUN R -e 'install.packages("remotes")'
RUN R -e 'remotes::install_version("renv", version = "1.0.3")'
COPY renv.lock.prod renv.lock
RUN R -e 'renv::restore()'
Dockerfile
Dockerfile
builds on this base image to set up the gap app-package and its dependencies.
show/hide Dockerfile
FROM gap_base
COPY renv.lock.prod renv.lock
RUN R -e 'options(renv.config.pak.enabled = FALSE);renv::restore()'
COPY gap_*.tar.gz /app.tar.gz
RUN R -e 'remotes::install_local("/app.tar.gz",upgrade="never")'
RUN rm /app.tar.gz
EXPOSE 80
USER rstudio
CMD R -e "options('shiny.port'=80,shiny.host='0.0.0.0');library(gap);gap::run_app()"
README
README
contains the commands to run in the Terminal to build and run the Docker image.
show/hide README
docker build -f Dockerfile_base --progress=plain -t gap_base .
docker build -f Dockerfile --progress=plain -t gap:latest .
docker run -p 80:80 gap:latest
# then go to 127.0.0.1:80
gap_0.0.0.9000.tar.gz
gap_0.0.0.9000.tar.gz
is the app-package to deploy in the Docker container.
Dependencies
Below is a quick overview of the dependencies in gap
.
NAMESPACE
The namespace file for gap
is importing shiny
(and the .data
operator from rlang
).
# Generated by roxygen2: do not edit by hand
export(run_app)
import(shiny) importFrom(rlang,.data)
As you can see, we’re only exporting the run_app()
function from gap
.
Imports
The Imports
field in the DESCRIPTION
file lists the following:
Imports:
config (>= 0.3.1),
ggplot2,
golem (>= 0.3.5),
rlang,
shiny (>= 1.7.4),
stringr, tools
As we can see, golem
apps add golem
as a dependency:
::local_deps_explain(
pakdeps = 'golem',
root = "_apps/gap")
ℹ Loading metadata database
✔ Loading metadata database ... done
-> golem gap
Recap
If you typically build Shiny apps in a single app.R
file (or in ui.R
and server.R
files), the golem
framework might seem overwhelming. I’ll give a quick overview of some areas I found confusing when I started using goelm
:
dev/
containsgolem
‘s ’guided tour’ scripts (01_start.R
,02_dev.R
,03_deploy.R
) andrun_dev.R
R/
: the primary app files for the UI and server are stored in theR/
folder (R/app_ui.R
,R/app_server.R
,R/run_app.R
), as well as the configuration function (R/app_config.R
)golem
apps are run using thegap::run_app()
function (included in theR/
folder)While developing,
golem
also comes with arun_dev
function that reads theR/run_dev.R
file and evaluates the code.The
inst/
folder holds thegolem-config.yml
and location of any external app files.Deploying the application can be done with a single function:
rsconnect::deployApp()
Generally speaking, golem
’s start-up scripts save time and serve as a gentle introduction to some of the functions used in R package development.
The add_
functions are an area where golem
really separates itself from standard R package development. Having dedicated Shiny development functions (and the app/inst/www
folder) reduces the cognitive overhead of mapping the standard R package development functions (i.e., those from usethis
and devtools
) into Shiny app-package development.
golem
is a popular framework for a reason–it’s designed to allow developers to build a shiny application and R package simultaneously. Added bonuses include taking advantage of RStudio’s build tools, great documentation, and user-guides..
Below is an overview of the features/functions in the golem
framework:
Feature | Arguments/Options | Description/Comments |
---|---|---|
dev/ scripts |
|
These files are automatically included in new golem apps and walk through set up, development, and deployment |
fill_desc() : fills DESCRIPTION file |
Arguments are passed as strings without having to worry about formatting (i.e., utils::person() ). |
Includes many necessary fields often overlooked when using usethis::create_package() |
attachment::att_amend_desc() |
Updates the package dependencies in the Imports field of DESCRIPTION |
Although not part of the golem package, attachment is built by the fine folks at ThinkR and makes managing dependencies smoother. |
set_golem_options() |
This sets a variety of options in the golem-config.yml file (most notably the name, version, and path to your app-package). |
|
use_recommended_tests() |
Create testthat infrastructure and adds a collection of boilerplate tests in the tests/testthat/ folder. |
|
use_utils_ui() & use_utils_server() |
with_test is set to TRUE |
Creates a collection of commonly used UI and server functions (and accompanying tests). |
add_module("name", fct, utils, export, with_test) |
|
This is one of the best features in An added bonus is a consistent file naming convention. |
add_fct() and add_utils() |
with_test : creates the accompanying tests/testthat/test-[name].R file. |
These are essentially wrappers for usethis::use_r() and usethis::use_test() |
Adding non-R code files:
|
Each
|
Each of these functions create the necessary files in the inst/app folder. |
Footnotes
New functions created with
golem::add_*()
functions are placed in theR/
folder with a@noRd
tag by default (this behavior can be changed with theexport
argument).↩︎This contains the
movies.RData
file for the original Shiny application.↩︎use_recommended_tests()
adds thespelling
package to ourDESCRIPTION
and updates theWORDLIST
. The testsgolem
creates in thetests/testthat/
folder can serve as a nice guide for users new totestthat
.↩︎The
covrpage
package is not on CRAN, but the development version always seems to work.↩︎rhub::check_for_cran()
is “deprecated and defunct”, userhub::rhubv2()
instead.↩︎