install.packages("devtools")
::install_github("mjfrigaard/gap") devtools
golem shiny app-packages
Building a production-grade shiny app: whole-game
This post walks through building a shiny application using the golem
framework. golem
is a ’an opinionated framework for building production-grade shiny applications’–I’ll explore some of the opinions (and offer my opinion on adopting these opinions).
For consistency, I’ll be using the application from the RStudio’s Building Web Applications with Shiny course. These materials are a great resource if you’re new to shiny–even if you’re aren’t, it’s still worth checking out–plus it’s free!
The golem text is also a fantastic resource, but I found myself using the golem
website as a great ‘quick reference.’ If you’re unfamiliar with R package development, I recommend bookmarking R packages–this is a great resource you’ll return to often.
Outline
I’ve organized the app-package development process into three areas: Start, Build, and Use.
Start covers the required steps to launch your
golem
project in the RStudio IDE, common R package files and folders, and other setup considerations.Build covers the app-package development process, which includes writing and storing code, data, external resources (like CSS or JavaScript), testing, etc.
Use shows how to launch a
golem
application locally (i.e., within the RStudio IDE), common workflow tips, and anything I found confusing while building the application.
dev/
scripts
New golem
apps automatically open the 01_start.R
script from the dev/
folder. This is the first of three .R scripts that serve as a ‘guided tour’ of the golem
framework (01_start.R
, 02_dev.R
, and 03_deploy.R
):
The run_dev.R
is also in the dev/
folder, but it’s for running a development version of your app (more on this later).
Start
To create a new golem
app from the console, enter the following:
install.packages("golem")
library(golem)
::create_golem(path = "gap") golem
If you’re creating a golem
app using the New Project Wizard, the following defaults are available:
When the new project opens, the initial folder structure for your new golem
application is below:
show/hide golem folder structure
app-name/
├── 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
├── [app-name].Rproj
├── inst
│ ├── app
│ │ └── www
│ │ └── favicon.ico
│ └── golem-config.yml
└── man
└── run_app.Rd
7 directories, 14 files
Begin 01_start.R
The dev/01_start.R
file is covered in the first few sections of the golem
text, but I prefer the package website as a reference because it walks through each dev/
script (with links to the golem
functions).
Fill
the
DESCRIPTION
There are three files in a new golem app-package–NAMESPACE
, DESCRIPTION
, and [app name].Rproj
. dev/01_start.R
starts by building the DESCRIPTION
file with golem::fill_desc()
DESCRIPTION
fill_desc()
is from the desc
package, and the sections are entered in a key = "value"
format
Example
DESCRIPTION
file contents:::fill_desc( golempkg_name = "gap", pkg_title = "An example goelm app", pkg_description = "A working example of the golem package.", author_first_name = "Martin", author_last_name = "Frigaard", author_email = "mjfrigaard@pm.me", repo_url = NULL # The URL of the GitHub Repo (optional) )
show/hide output from golem::fill_desc()
✔ Setting `golem_version` to 0.0.0.9000 ✔ Setting `golem_name` to gap ✔ DESCRIPTION file modified
Set
{golem}
options
The golem::set_golem_options()
wraps a collection of golem
‘s ’opinionated’ application development and configuration options.
::set_golem_options() golem
show/hide output from golem::set_golem_options()
── Setting {golem} options in `golem-config.yml` ────────────────────────────────────────────────────────────────────
✔ Setting `golem_name` to gap
✔ Setting `golem_wd` to golem::pkg_path()
You can change golem working directory with set_golem_wd('path/to/wd')
✔ Setting `golem_version` to 0.0.0.9000
✔ Setting `app_prod` to FALSE
── Setting {usethis} project as `golem_wd` ──────────────────────────────────────────────────────────────────────────
✔ Setting active project to '/projects/gap'
set_golem_options()
is a wrapper for a collection of golem
option functions (I’ve included each function and a brief description of their behavior below):
show/hide golem options
::set_golem_options(
golemgolem_name = golem::pkg_name(), # name of the app-package in DESCRIPTION
golem_version = golem::pkg_version(), # version in DESCRIPTION
golem_wd = golem::pkg_path(), # package root when starting a golem
app_prod = FALSE, # production mode?
talkative = TRUE, # Should the messages be printed to the console?
config_file = golem::get_current_config(golem_wd) # golem-config.yml location
)
The new config file is located in the inst/
folder.
Install
required
dev
dependencies
The golem::install_dev_deps()
function makes sure the following packages are installed (I’ve grouped them into categories):
Development
Documentation & testing
attachment
(deal with package dependencies)desc
(ParseDESCRIPTION
files)testthat
(unit testing your code)
Internals
rstudioapi
(interacting with RStudio IDE)processx
(execute and control subprocesses from R)
Files & paths
Deploy
dockerfiler
(deploying apps with docker)rsconnect
(deploy shiny apps with RSConnect)
::install_dev_deps() golem
Below is an example with dockerfiler
:
show/hide output from golem::install_dev_deps()
ℹ The package "dockerfiler" is required.
✖ Would you like to install it?
1: Yes
2: No
Selection: 1
✔ Updated metadata database: 5.32 MB in 12 files.
✔ Updating metadata database ... done
→ Will install 1 package.
→ Will download 1 CRAN package (104.29 kB).
+ dockerfiler 0.2.1 ⬇ (104.29 kB)
ℹ Getting 1 pkg (104.29 kB)
✔ Got dockerfiler 0.2.1 (x86_64-apple-darwin17.0) (104.29 kB)
✔ Downloaded 1 package (104.29 kB)in 1.1s
✔ Installed dockerfiler 0.2.1 (54ms)
✔ 1 pkg + 40 deps: kept 40, added 1, dld 1 (104.29 kB) [20.2s]
Create
Common
Files
The ‘Create Common Files’ section of dev/01_start.R
contains many of the functions and files covered in the ‘Whole Game’ section of R packages:
LICENSE
::use_mit_license("Golem User") usethis
✔ Adding 'MIT + file LICENSE' to License ✔ Writing 'LICENSE' ✔ Writing 'LICENSE.md' ✔ Adding '^LICENSE\\.md$' to '.Rbuildignore'
LICENSE
fileREADME.Rmd
::use_readme_rmd() usethis
✔ Writing 'README.Rmd' ✔ Adding '^README\\.Rmd$' to '.Rbuildignore' • Update 'README.Rmd' to include installation instructions.
The README.md is built with
devtools::build_readme()
::build_readme() devtools
ℹ Installing gap in temporary library ℹ Building /projects/gap/README.Rmd
CODE_OF_CONDUCT.md
::use_code_of_conduct() usethis
✔ Writing 'CODE_OF_CONDUCT.md' ✔ Adding '^CODE_OF_CONDUCT\\.md$' to '.Rbuildignore' • You may also want to describe the code of conduct in your README: ## Code of Conduct Please note that the gap project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. [Copied to clipboard]
Paste of the code of conduct in the README.md is rebuild with
devtools::build_readme()
::build_readme() devtools
ℹ Installing gap in temporary library ℹ Building /projects/gap/README.Rmd
Lifecycle badge
::use_lifecycle_badge("Experimental") usethis
✔ Adding Lifecycle: experimental badge to 'README.Rmd' • Re-knit 'README.Rmd' with `devtools::build_readme()`
Rebuild the README.md with
devtools::build_readme()
::use_lifecycle_badge("Experimental") usethis
ℹ Installing gap in temporary library ℹ Building /projects/gap/README.Rmd
golem
lifecycle badgeNEWS.md
::use_news_md(open = FALSE) usethis
✔ Writing 'NEWS.md'
NEWS.md
fileGit:
usethis::use_git()
will ask if you’d like to commit the files in your golem app to a repo of the same name:::use_git() usethis
✔ Setting active project to '/projects/gap' ✔ Initialising Git repo ✔ Adding '.Rproj.user', '.Rhistory', '.Rdata', '.httr-oauth', '.DS_Store', '.quarto' to '.gitignore' There are 16 uncommitted files: * '.gitignore' * '.here' * '.Rbuildignore' * 'CODE_OF_CONDUCT.md' * 'DESCRIPTION' * 'dev/' * 'gap.Rproj' * 'inst/' * 'LICENSE' * 'LICENSE.md' * ... Is it ok to commit them? 1: Definitely 2: Negative 3: Not now Selection: 1
To initialize the Git pane, you’ll need to restart RStudio (in the next dialogue)
✔ Adding files ✔ Making a commit with message 'Initial commit' • A restart of RStudio is required to activate the Git pane Restart now? 1: Absolutely 2: Negative 3: No Selection: 1
Init
Testing
Infrastructure
golem::use_recommended_tests()
with set up the testthat
architecture for unit tests.
::use_recommended_tests() golem
✔ Setting active project to '/projects/gap'
✔ Adding 'testthat' to Suggests field in DESCRIPTION
✔ Adding '3' to Config/testthat/edition
✔ Creating 'tests/testthat/'
✔ Writing 'tests/testthat.R'
• Call `use_test()` to initialize a basic test file and open it for editing
It also adds a few words to the WORDLIST
file in the inst
folder:
✔ Adding 'spelling' to Suggests field in DESCRIPTION
✔ Adding 'en-US' to Language
The following words will be added to the wordlist:
- Lifecycle
- goelm
- golem
Are you sure you want to update the wordlist?
1: Yes
2: No
Selection: 1
Added 3 and removed 0 words in /projects/gap/inst/WORDLIST
Updated /projects/gap/tests/spelling.R
• Run `devtools::check()` to trigger spell check
✔ Tests added
golem::use_recommended_tests()
also provides some examples for testing the UI, server, and other golem
functions:
show/hide unit tests in test-golem-recommended.R
test_that("app ui", {
<- app_ui()
ui ::expect_shinytaglist(ui)
golem# Check that formals have not been removed
<- formals(app_ui)
fmls for (i in c("request")) {
expect_true(i %in% names(fmls))
}
})
test_that("app server", {
<- app_server
server expect_type(server, "closure")
# Check that formals have not been removed
<- formals(app_server)
fmls for (i in c("input", "output", "session")) {
expect_true(i %in% names(fmls))
}
})
test_that(
"app_sys works",
{expect_true(
app_sys("golem-config.yml") != ""
)
}
)
test_that(
"golem-config works",
{<- app_sys("golem-config.yml")
config_file skip_if(config_file == "")
expect_true(
get_golem_config(
"app_prod",
config = "production",
file = config_file
)
)expect_false(
get_golem_config(
"app_prod",
config = "dev",
file = config_file
)
)
}
)
# Configure this test to fit your need.
# testServer() function makes it possible to test code in server functions and modules, without needing to run the full Shiny application
testServer(app_server, {
# Set and test an input
$setInputs(x = 2)
sessionexpect_equal(input$x, 2)
# Example of tests you can do on the server:
# - Checking reactiveValues
# expect_equal(r$lg, 'EN')
# - Checking output
# expect_equal(output$txt, "Text")
})
# Configure this test to fit your need
test_that(
"app launches",
{::expect_running(sleep = 5)
golem
} )
These tests pass right out of the box, and they give a little ‘sneak preview’ of how the golem
framework works.
show/hide results from unit tests in test-golem-recommended.R
==> Testing R file using 'testthat'
ℹ Loading gap
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 9 ]
Loading required package: shiny
[ FAIL 0 | WARN 0 | SKIP 1 | PASS 10 ]
── Skipped tests (1) ─────────
• interactive() is not TRUE
(1):
test-golem-recommended.R:72:5
Test complete
Favicon
A favicon is a the little image that shows up on your browser tab or address bar. golem
has a default favicon in the inst/app/
folder:
inst/
└── app
└── www
└── favicon.ico
3 directories, 1 file
The inst/
folder serves a specific purpose in golem apps (and R packages), which I’ll cover more below. For our purpose, the golem::use_favicon()
function can use the existing image:
::use_favicon(path = "inst/app/www/favicon.ico") golem
The output introduces another common golem
function: golem_add_external_resources()
✔ favicon.ico created at
/projects/gap/inst/app/www/favicon.ico
Favicon is automatically linked in app_ui via `golem_add_external_resources()`
This function is used to add external resources to your application (and will come up often during development).
Add
helper
functions
Most applications will have two types of code–shiny
functions for running your application (app functions), and functions that do everything else (or utility functions). The golem
framework further distinguishes utility functions into two types: 1) “small functions that are reused throughout the app” (with a utils_
prefix), and 2) “larger functions, which are more central to the application” (with a fct_
prefix).
The descriptions below are from the text and provide examples for the two types of utility functions:
utils_
functions: “…thehexmake
app has two of these files,R/utils_ui.R
andR/utils_server.R
, in which you will find small functions that are reused throughout the app.”
fct_
functions: “…inhexmake
, you will findR/fct_mongo.R
, which is used to handle all the things related to connecting and interacting with the Mongodb database.”
use_utils_ui()
The golem::use_utils_ui()
function will add a collection of utility functions for the UI. Including with_test = TRUE
will add a test for these functions.
::use_utils_ui(with_test = TRUE) golem
✔ File created at /projects/gap/R/golem_utils_ui.R
✔ Utils UI added
✔ File created at /projects/gap/tests/testthat/test-golem_utils_ui.R
✔ Tests on utils_server added
use_utils_server()
golem
also includes a set of functions for the application server (or server modules), golem::use_utils_server()
. The with_test = TRUE
will also add a test to the tests/testthat/
folder:
::use_utils_server(with_test = TRUE) golem
✔ File created at /projects/gap/R/golem_utils_server.R
✔ Utils server added
✔ File created at /projects/gap/tests/testthat/test-golem_utils_server.R
✔ Tests on utils_server added
End 01_start.R
This is the final function in the dev/01_start.R
file. In the next golem
dev script (dev/02_dev.R
), I’ll cover development of your golem
application.
Build
The dev/02_dev.R
file is appropriately titled, ‘Engineering
’, and unlike the functions in the first script, these functions will be used repeatedly for creating files in the R/
and inst/
folders.
Begin 02_dev.R
App files
Let’s start by examining the contents of the R/
folder in our new golem
application:
R/
├── app_config.R
├── app_server.R
├── app_ui.R
└── run_app.R
1 directory, 4 files
golem
applications structure shiny apps into three files: R/app_ui.R
, R/app_server.R
, and R/run_app.R
.
The
R/app_ui.R
andR/app_server.R
scripts aregolem
’s version ofui.R
andserver.R
R/run_app.R
is a standalone app function, andR/app_config.R
is used to set/getgolem
configuration settings (which we will cover more below).
app_ui.R
app_ui.R
wraps the UI functions in shiny::tagList()
(you’ll see this function in shiny UI module functions, too).
show/hide app_ui()
<- function(request) {
app_ui tagList(
# Leave this function for adding external resources
golem_add_external_resources(),
# Your application UI logic
fluidPage(
h1("gap")
)
) }
app_ui()
also contains a call to golem_add_external_resources()
, which we used above to add the favicon image.
show/hide golem_add_external_resources()
<- 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.R
The contents of app_server.R
file looks similar to a standard shiny server function:
show/hide app_server()
<- function(input, output, session) {
app_server # Your application server logic
}
app_config.R
The app_config.R
file contains the “internal mechanics for golem
, notably for referring to values in the inst/
folder, and to get values from the config
file in the inst/
folder”. Two functions drive the internal mechanics of your golem
app: app_sys()
and get_golem_config()
app_sys()
is a wrapper around thesystem.file()
function, and it’s used to “quickly refer to the files inside theinst/
folder”
show/hide app_sys()
<- function(...) {
app_sys system.file(..., package = "gap")
}
get_golem_config()
is where you’ll setgolem
configuration options (covered here in the text).
show/hide get_golem_config()
<- 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
) }
get_golem_config()
reads the inst/golem-config.yml
configuration file:
default:
golem_name: gap
golem_version: 0.0.0.9000
app_prod: no
production:
app_prod: yes
dev:
golem_wd: !expr here::here()
golem-config.yml
gives me access to the app version, name, and (development) working directory. This file is designed to add “production-only elements” and be “shareable across golem
projects”
run_app.R
run_app.R
is the exported function I’ll use to run my golem
app after loading/documenting/installing the package:
::load_all(".") devtools
ℹ Loading gap
::document() devtools
ℹ Updating gap documentation
ℹ Loading gap
Restarting R session...
library(gap)
::run_app() gap
Dependencies
Dependency management is a necessary evil of package development. shiny
has a large ecosystem of user-written add-on packages. To use the code from add-on packages in our application, we need a way to keep track of which function belongs to which package.
The DESCRIPTION
file manages package-level dependencies. The Imports
field in the DESCRIPTION
file specifies packages that my package uses, so the functions from these packages will be available for my package, but not for users (unless they use the ::
operator or load the package themselves).
The NAMESPACE
file manages function-level access. The NAMESPACE
file manages the functions that are exported from my package (i.e., functions available to users who install my package), and the functions my package imports from other packages.
The golem
text describes the difference between these files in the following way,
“The
DESCRIPTION
file dictates which packages have to be installed when your application is installed”“The
NAMESPACE
file describes how your app interacts with the R session at run time, i.e. when your application is launched”
The attachment
package makes it easier to manage the dependencies in your golem
application. It does this by looking through the files in your package to make sure everything is properly documented in the NAMESPACE
and DESCRIPTION
file (note that these two files are not equivalent or connected).
The att_amend_desc()
function removes a lot of the tedium involved in dependency management:
::att_amend_desc() attachment
This function adds the appropriate parameters to golem-config.yml
and sets up function documentation in the DESCRIPTION
file
Saving attachment parameters to yaml config file
Updating [app-name] documentation
Setting `RoxygenNote` to "7.2.3"
It loads the contents of our package (i.e. devtools::load_all()
) and writes the NAMESPACE
file
ℹ Loading [app-name]
Writing NAMESPACE
It also writes the help files in the man/
folder.
Writing run_app.Rd
Add modules
golem
has functions for quickly creating modules and utility/helper functions in the R/
folder.
add_module()
::add_module(name = "name_of_module1", with_test = TRUE)
golem::add_module(name = "name_of_module2", with_test = TRUE) golem
Add helper functions
golem apps differentiates two types of helper functions: uils_
and fct_
.
add_utils()
uils_
functions: “small functions that might be used several times in the application” … “more ‘topic centered’, in the sense that they gather functions that relate to a specific feature of the application(+”
::add_utils("helpers", with_test = TRUE) golem
add_fct()
fct_
functions: “larger functions that are more central to the application” … “more used as a place to put miscellaneous functions”
::add_fct("helpers", with_test = TRUE) golem
with_test = TRUE
ensures these functions will also create test files in tests/
golem::add_
functionsExternal resources
dev/02_dev.R
includes golem
wrappers for including CSS, JavaScript, and SASS files to the inst/app/www/
folder:
JavaScript files
You can add JavaScript to your application using the golem::add_js_file("script")
and golem::add_js_handler("handlers")
functions.
The golem
text has an entire chapter dedicated to JavaScript (which is worth reading).
App styling
You can add CSS or SASS styling to your application using the golem::add_css_file("custom")
and golem::add_sass_file("custom")
functions, too.
golem
external resourcesAdd internal datasets
If you application uses data, you can add it to your application with the usethis
functions (use_data_raw()
or use_data()
). I recommend reading the data section of R packages (and this section on adding data to inst/extdata
).
golem
app dataLocations of data in golem
app-packages
inst/extdata
External data you’d like to make available in your package should be stored in inst/extdata/
This is where I’ve placed the movies
file:
inst/extdata/
└── movies.RData
1 directory, 1 file
use_data_raw()
The data-raw/
folder is for the ‘data-creating script’ that was used to create the version of the data in your app-package:
- I’ll create a raw data file for
movies
.
::use_data_raw("movies") usethis
Newly created
.R
scripts inuse_data_raw()
will have a call touse_data()
. See below:✔ Writing 'data-raw/movies.R' • Modify 'data-raw/movies.R' • Finish the data preparation script in 'data-raw/movies.R' • Use `usethis::use_data()` to add prepared data to package
use_data()
The objects created from the .R
files in data-raw/
are stored in the data/
folder (and any other data you need in your app-package).
“store R objects and make them available to the user…in
data/
”
For example, in data-raw/movies.R
, I load the movies data from inst/extdata/
, and save movies
to the data/
folder:
## code to prepare `movies` dataset goes here
load("inst/extdata/movies.RData")
::use_data(movies, overwrite = TRUE) usethis
The output is below:
✔ Saving 'movies' to 'data/movies.rda' • Document your data (see 'https://r-pkgs.org/data.html')
Follow this guide to document your datasets.
show/hide movies data documentation
#' Movies dataset
#'
#' A dataset containing movie rankings from IMDB and Rotten Tomatoes. Original
#' source found [here](https://rstudio-education.github.io/shiny-course/)
#'
#' @format A data frame with 651 rows and 34 variables:
#'
#' \describe{
#' \item{title}{movie title}
#' \item{title_type}{title type (Documentary, Feature Film, TV, Movie)}
#' \item{genre}{movie genre (Action & Adventure, Animation,
#' Art House & International, Comedy, Documentary, Drama, Horror,
#' Musical & Performing Arts, Mystery & Suspense, Other,
#' Science Fiction & Fantasy)}
#' \item{runtime}{length of film (in minutes)}
#' \item{mpaa_rating}{G, NC-17, PG, PG-13, R, Unrated}
#' \item{studio}{studio movie was filmed in}
#' \item{thtr_rel_date}{Theatre release date}
#' \item{thtr_rel_year}{Theatre release year}
#' \item{thtr_rel_day}{Theatre release day}
#' \item{dvd_rel_date}{DVD release date}
#' \item{dvd_rel_year}{DVD release year}
#' \item{dvd_rel_month}{DVD release month}
#' \item{dvd_rel_day}{DVD release day}
#' \item{imdb_rating}{IMDB rating}
#' \item{imdb_num_votes}{IMDB number of votes}
#' \item{critics_rating}{Critics rating}
#' \item{critics_score}{Critics score}
#' \item{audience_rating}{Audience rating}
#' \item{audience_score}{Audience score}
#' \item{best_pic_nom}{Best picture nomination?}
#' \item{best_pic_win}{Best picture win?}
#' \item{best_actor_win}{Best actor win?}
#' \item{best_actress_win}{Best actrss win?}
#' \item{best_dir_win}{Best director win?}
#' \item{top200_box}{Top 200 box-office?}
#' \item{director}{Film director name}
#' \item{actor1}{Actor 1 name}
#' \item{actor2}{Actor 2 name}
#' \item{actor3}{Actor 3 name}
#' \item{actor4}{Actor 4 name}
#' \item{actor5}{Actor 5 name}
#' \item{imdb_url}{IMDB website url}
#' \item{rt_url}{Rotten Tomatoes website url}
#' }
"movies"
The fivethirtyeight
package also has great examples of documented datasets.
After loading and documenting your package, you can view structure and variable names of the movies
data by entering ??movies
in the Console:
movies
data documentation
gap::movies
stored in R/data.R
Tests
The tests/
folder was created in dev/01_start.R
with golem::use_recommended_tests()
. This function is a wrapper around usethis::use_testthat()
, and it sets up the tests/
folder:
tests
├── spelling.R
├── testthat/ └── testthat.R
In dev/02_dev.R
, the golem::add_module()
and golem::add_utils()
/golem::add_fct()
functions also include a with_test = TRUE
argument, which creates a test file in the tests/
folder.
These functions add two test files in tests/testthat/
:
tests/testthat
├── test-golem-recommended.R
├── test-golem_utils_server.R
└── test-golem_utils_ui.R
1 directory, 3 files
These files test the functions in the files golem_utils_ui.R
and golem_utils_server.R
, and we can run them independently with testthat::test_file()
test_file("tests/testthat/test-golem_utils_server.R")
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 13 ]
test_file("tests/testthat/test-golem_utils_ui.R")
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 51 ]
Documentation
R package vignettes contain high-level, long-form documentation for your package. These R Markdown documents combine narrative text and code that describe how the ‘parts’ (functions, data, etc.) of the package behave. In app-packages, vignettes might included the following information:
For shiny
app-packages, the following types of documentation might be included in the vignettes:
Introduction to the package
Installation guide
Usage examples
Description of the application and modules
Application workflow
Data preparation
Troubleshooting
FAQs
Advanced usage
Details on functions and datasets
References and Contact information
To create a new vignette, run usethis::use_vignette("NAME OF VIGNETTE")
:
::use_vignette("gap") usethis
✔ Setting active project to '/Users/mjfrigaard/projects/gap'
✔ Adding 'knitr' to Suggests field in DESCRIPTION
✔ Adding 'rmarkdown' to Suggests field in DESCRIPTION
✔ Adding 'knitr' to VignetteBuilder
✔ Adding 'inst/doc' to '.gitignore'
✔ Creating 'vignettes/'
✔ Adding '*.html', '*.R' to 'vignettes/.gitignore'
✔ Writing 'vignettes/gap.Rmd'
• Modify 'vignettes/gap.Rmd'
The vignette file opens with the title matching the argument passed to use_vignette()
. To build the vignettes in your package, run:
::build_vignettes() devtools
Code Coverage
Test code coverage measures the extent to which the test cases cover the possible execution paths in the package codebase–its a way to ensure that the tests are robust enough to verify that the code behaves as expected.
There are two functions/methods used to calculate code coverage in your application: usethis::use_coverage()
and covrpage::covrpage()
.
usethis::use_coverage()
use_coverage()
is part of the usethis
package and can be run interactively during development:
::use_coverage() usethis
✔ Setting active project to '/Users/mjfrigaard/projects/gap'
covrpage::covrpage()
Install covrpage
using the following:
# install.packages("remotes")
# remotes::install_github('yonicd/covrpage',
# force = TRUE, quiet = TRUE)
library(covrpage)
To use covrpage
, run the following:
::covrpage() covrpage
::covrpage() covrpage
This sets up the README
in the tests/
folder.
tests/
├── README.md <- covrpage README!
├── spelling.R
├── testthat
│ ├── test-golem-recommended.R
│ ├── test-golem_utils_server.R
│ └── test-golem_utils_ui.R └── testthat.R
The test coverage vignette is created with use_covrpage_vignette()
::use_covrpage_vignette() covrpage
copying tests_and_coverage.Rmd into ./vignettes
adding inst/doc to .gitignore
adding knitr,rmarkdown to Suggests field in ./DESCRIPTION adding VignetteBuilder: knitr to ./DESCRIPTION
You can view the covrpage
for this app here
CI
Continuous integration can be handled with one of the GitHub Actions functions (make sure you’re using Git). See the usethis
website for more information on using GitHub Actions.
If you’re using another CI management system, the following options are available.
Github Actions CI
Set up GitHub actions
# GitHub Actions ::use_github_action() usethis
# Chose one of the three ::use_github_action_check_release() usethis::use_github_action_check_standard() usethis::use_github_action_check_full() usethis
# Add action for PR ::use_github_action_pr_commands() usethis
Other CI Options
Travis CI
::use_travis() usethis::use_travis_badge() usethis
AppVeyor
::use_appveyor() usethis::use_appveyor_badge() usethis
Circle CI
::use_circleci() usethis::use_circleci_badge() usethis
Jenkins
::use_jenkins() usethis
GitLab CI
::use_gitlab_ci() usethis
End 02_dev.R
This concludes the 02_dev.R
file. It’s likely you’ll return to this file repeatedly for various functions during development, so I’d leave it in the dev/
folder for future reference.
Use
In this section, I’ll go over the functions used during application development, how to launch the application (locally, in the IDE), and the third and final dev/
script (dev/03_deploy.R
), which is full of options for deploying your shiny app.
Writing code
While developing, I find the add_*
functions are incredibly helpful (add_module()
, add_utils()
, and add_fct()
). New modules functions can be created with golem::add_module("name")
along with their tests and utility functions.
add_module(name = "plot", utils = "server", with_test = TRUE)
✔ File created at R/mod_plot.R
✔ File created at R/mod_plot_utils_server.R
✔ File created at tests/testthat/test-mod_plot.R
The functions added to the
R/
folder include@noRd
by default (which must be removed create the.Rd
files in theman/
folder)# UI module template ------------------- #' test UI Function #' #' @description A shiny Module. #' #' @param id,input,output,session Internal parameters for {shiny}. #' #' @noRd <- this one! #' #' @importFrom shiny NS tagList # server module template --------------- #' test Server Functions #' #' @noRd <- and this one!
UI module functions end with a
_ui
suffix:show/hide mod_plot_ui()
#' plot UI Function #' #' @param id #' #' @return shiny UI module #' @export mod_plot_ui #' #' @importFrom shiny NS tagList tags #' @importFrom shiny plotOutput verbatimTextOutput <- function(id) { mod_plot_ui <- shiny::NS(id) ns ::tagList( shiny::tags$br(), shiny::tags$blockquote( shiny::tags$em( shiny::tags$h6( shiny"The code for this application comes from the ", ::tags$a("Building web applications with Shiny", shinyhref = "https://rstudio-education.github.io/shiny-course/" ),"tutorial" ) ) ),::plotOutput(outputId = ns("scatterplot")) shiny ) }
Server module functions end with a
_server
suffix:show/hide mod_plot_server()
#' plot Server Functions #' #' @param id module id #' @param var_inputs inputs from mod_var_input #' #' @return shiny server module #' @export mod_plot_server #' #' @importFrom shiny NS moduleServer reactive #' @importFrom tools toTitleCase #' @importFrom shiny renderPlot #' @importFrom stringr str_replace_all #' @importFrom ggplot2 labs theme_minimal theme <- function(id, var_inputs) { mod_plot_server ::moduleServer(id, function(input, output, session) { shiny<- gap::movies movies <- shiny::reactive({ inputs <- tools::toTitleCase(var_inputs$plot_title()) plot_title list( x = var_inputs$x(), y = var_inputs$y(), z = var_inputs$z(), alpha = var_inputs$alpha(), size = var_inputs$size(), plot_title = plot_title ) }) $scatterplot <- shiny::renderPlot({ output<- point_plot( plot 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_plot_ui("plot_1") ## To be copied in the server # mod_plot_server("plot_1")
See the utility function and other module in the gap
app-package on GitHub below:
Include tests for new modules and functions using the
with_test = TRUE
argumenttests/testthat/ ├── _snaps ├── test-golem-recommended.R ├── test-golem_utils_server.R ├── test-golem_utils_ui.R ├── test-mod_plot.R ├── test-mod_plot_utils_server.R └── test-mod_var_input.R 2 directories, 6 files
The updated covrpage
report is available here
Adding resources
If I want to include other files (like images), I can add these files to inst/app/www/
,
inst/app/www
# add icon
inst/app
└── www/ └── shiny.png
Then I can include the path in the UI (see example below):
# add icon
::tags$img(src = "www/shiny.png") shiny
addResourcePath()
If I wanted to include images in their own folder (like images/
), I can use golem::addResourcePath()
to add the name of the sub-folder to inst/app/
# add icon
::add_resource_path(
golemprefix = 'images',
directoryPath = system.file('app/images',
package = 'gap'))
Now I can add the image file to the inst/app/www/images/
folder and include the following code in the UI:
# add icon
::tags$img(src = "www/images/golem-hex.png") shiny
In
R/app_ui.R
, theapp_ui()
function contains the UI layout functions (fluidPage()
,sidebarLayout()
, etc.), and a call togolem_add_external_resources()
:show/hide app_ui()
#' The application User-Interface #' #' @param request Internal parameter for `{shiny}`. #' DO NOT REMOVE. #' @import shiny #' @keywords internal <- function(request) { app_ui ::tagList( shiny# Leave this function for adding external resources golem_add_external_resources(), # Your application UI logic ::fluidPage( shiny::tags$h1("gap"), shiny::sidebarLayout( shiny::sidebarPanel( shinymod_var_input_ui("vars") ),::mainPanel( shiny# add shiny hex in www/ ::tags$img(src = "www/shiny.png"), shinymod_plot_ui("plot"), # add golem hex (in www/images/) ::fluidRow( shiny::tags$em(shiny::tags$h4( shiny"Brought to you by: ", ::tags$img(src = "www/images/golem-hex.png") shiny )) ) ) ) ) ) }
The
golem_add_external_resources()
function is below:golem_add_external_resources()
# this is also included in the app_ui.R script <- 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() ) }
Now when I run devtools::load_all()
, devtools::document()
, install/restart, and load the package, I see the images properly rendered with the application:
Begin 03_deploy.R
The final step in the guided tour contains functions for deploying a new application to Posit Connect or Docker (it opens automatically after completing the dev/02_dev.R
)
Run
checks
These functions are part of the package development process. devtools::check()
should be run frequently (I run it after creating a new .R file or creating a new test).
If you plan on submitting a package to CRAN, the rhub::check_for_cran()
function will create a ‘to-do’ list of CRAN comments.
## Run checks ----
## Check the package before sending to prod
::check()
devtools::check_for_cran() rhub
devtools::check()
: “It’s counter-intuitive but the key to minimizing this pain is to runR CMD check
more often: the sooner you find a problem, the easier it is to fix” - R packagesThe outputs from the initial
devtools::check()
are below (I’ve split it up into sections)Documenting
show/hide check() Documenting output
══ Documenting ═══════════════════════════════════════════════════════════ ℹ Updating gap documentation ℹ Loading gap ══ Building ═════════════════════════════════════════════════════════════ Setting env vars: • CFLAGS : -Wall -pedantic -fdiagnostics-color=always • CXXFLAGS : -Wall -pedantic -fdiagnostics-color=always • CXX11FLAGS: -Wall -pedantic -fdiagnostics-color=always • CXX14FLAGS: -Wall -pedantic -fdiagnostics-color=always • CXX17FLAGS: -Wall -pedantic -fdiagnostics-color=always • CXX20FLAGS: -Wall -pedantic -fdiagnostics-color=always ── R CMD build ────────────────────────────────────────────────────────── ✔ checking for file '/projects/apps/gap/DESCRIPTION' ... ─ preparing ‘gap’: (561ms) ✔ checking DESCRIPTION meta-information ... ─ installing the package to build vignettes ✔ creating vignettes (5.6s) ─ excluding invalid files Subdirectory 'R' contains invalid file names: ‘_disable_autoload.R’ ─ checking for LF line-endings in source and make files and shell scripts ─ checking for empty or unneeded directories ─ building ‘gap_0.0.0.9000.tar.gz’
Checking
show/hide check() Checking output
══ Checking ══════════════════════════════════════════════════════════════ Setting env vars: • _R_CHECK_CRAN_INCOMING_REMOTE_ : FALSE • _R_CHECK_CRAN_INCOMING_ : FALSE • _R_CHECK_FORCE_SUGGESTS_ : FALSE • _R_CHECK_PACKAGES_USED_IGNORE_UNUSED_IMPORTS_: FALSE • NOT_CRAN : true ── R CMD check ─────────────────────────────────────────────────────────── ─ using log directory ‘/private/var/folders/0x/x5wkbhmx0k74tncn9swz7xpr0000gn/T/RtmpYiEbLJ/file75936b43aefb/gap.Rcheck’ ─ using R version 4.2.3 (2023-03-15) ─ using platform: x86_64-apple-darwin17.0 (64-bit) ─ using session charset: UTF-8 ─ using options ‘--no-manual --as-cran’ ✔ checking for file ‘gap/DESCRIPTION’ ─ this is package ‘gap’ version ‘0.0.0.9000’ ─ package encoding: UTF-8 ✔ checking package namespace information ✔ checking package dependencies (2s) ✔ checking if this is a source package ... ✔ checking if there is a namespace ✔ checking for executable files (592ms) ✔ checking for hidden files and directories ✔ checking for portable file names ✔ checking for sufficient/correct file permissions ✔ checking whether package ‘gap’ can be installed (5s) ✔ checking installed package size ✔ checking package directory ... ✔ checking for future file timestamps ... ✔ checking ‘build’ directory ✔ checking DESCRIPTION meta-information ... ✔ checking top-level files ... ✔ checking for left-over files ✔ checking index information ... ✔ checking package subdirectories ... ✔ checking R files for non-ASCII characters ... ✔ checking R files for syntax errors ... ✔ checking whether the package can be loaded (1.2s) ✔ checking whether the package can be loaded with stated dependencies (1s) ✔ checking whether the package can be unloaded cleanly (998ms) ✔ checking whether the namespace can be loaded with stated dependencies (991ms) ✔ checking whether the namespace can be unloaded cleanly (1.2s) ✔ checking dependencies in R code (1.1s) ✔ checking S3 generic/method consistency (2.1s) ✔ checking replacement functions (1.1s) ✔ checking foreign function calls (1.1s) ✔ checking R code for possible problems (6.8s) ✔ checking Rd files (406ms) ✔ checking Rd metadata ... ✔ checking Rd line widths ... ✔ checking Rd cross-references ... ✔ checking for missing documentation entries (1.2s) W checking for code/documentation mismatches (2.4s) Data codoc mismatches from documentation object 'movies': Variables in data frame 'movies' Code: actor1 actor2 actor3 actor4 actor5 audience_rating audience_score best_actor_win best_actress_win best_dir_win best_pic_nom best_pic_win critics_rating critics_score director dvd_rel_date dvd_rel_day dvd_rel_month dvd_rel_year genre imdb_num_votes imdb_rating imdb_url mpaa_rating rt_url runtime studio thtr_rel_date thtr_rel_day thtr_rel_month thtr_rel_year title title_type top200_box Docs: actor1 actor2 actor3 actor4 actor5 audience_rating audience_score best_actor_win best_actress_win best_dir_win best_pic_nom best_pic_win critics_rating critics_score director dvd_rel_date dvd_rel_day dvd_rel_month dvd_rel_year genre imdb_num_votes imdb_rating imdb_url mpaa_rating rt_url runtime studio thtr_rel_date thtr_rel_day thtr_rel_year title title_type top200_box ✔ checking Rd \usage sections (3.4s) ✔ checking Rd contents ... ✔ checking for unstated dependencies in examples ... ✔ checking contents of ‘data’ directory ... ✔ checking data for non-ASCII characters ... ✔ checking LazyData ✔ checking data for ASCII and uncompressed saves ... ✔ checking installed files from ‘inst/doc’ ✔ checking files in ‘vignettes’ ... ✔ checking examples (2.4s) ✔ checking examples with --run-donttest (2.8s) ✔ checking for unstated dependencies in ‘tests’ ... ─ checking tests ...
spelling.R
show/hide check() spelling.R
✔ Running ‘spelling.R’ X Comparing ‘spelling.Rout’ to ‘spelling.Rout.save’ ... 6,47d5 < Potential spelling errors: < WORD FOUND IN < CONFIG get_golem_config.Rd:17 < Codecov README.md:11 < Config get_golem_config.Rd:5,25 < HTLM with_red_star.Rd:10 < IMDB movies.Rd:27,28,45,53 < Theatre movies.Rd:20,21,22 < UI mod_plot_ui.Rd:5,13,16 < mod_var_ui.Rd:5,13 < Ventura tests_and_coverage.Rmd:109 < ackage README.md:6 < actrss movies.Rd:36 < br rep_br.Rd:5,10,13,16 < tests_and_coverage.Rmd:82 < config app_sys.Rd:15 < get_golem_config.Rd:15,20,22 < tests_and_coverage.Rmd:39,89 < covr tests_and_coverage.Rmd:29,116 < covrpage tests_and_coverage.Rmd:24,117 < darwin tests_and_coverage.Rmd:108 < enurl tests_and_coverage.Rmd:83 < goelm title:1 < jq tests_and_coverage.Rmd:81 < jquery jq_hide.Rd:5,13 < li tests_and_coverage.Rmd:75,77 < macOS tests_and_coverage.Rmd:109 < na not_in.Rd:5,15 < not_na.Rd:5,13 < tests_and_coverage.Rmd:69 < reactiveValues rv.Rd:5,13 < reactiveValuesToList rvtl.Rd:5,13 < rv tests_and_coverage.Rmd:73 < rvtl tests_and_coverage.Rmd:73 < sys tests_and_coverage.Rmd:88 < tagRemoveAttributes tests_and_coverage.Rmd:78 < testthat tests_and_coverage.Rmd:49,115 < tibble point_plot.Rd:10 < ui tests_and_coverage.Rmd:37,41,54,74,74,75,75,76,76,77,77,78,78,79, 79,80,80,81, 81,82,82,83,83,84,84,85,85,86,92,93 < undisplay tests_and_coverage.Rmd:79 < ️ tests_and_coverage.Rmd:97
testthat.R
show/hide check() tests and coverage
✔ Running ‘testthat.R’ (2.9s) ✔ checking for unstated dependencies in vignettes ... ✔ checking package vignettes in ‘inst/doc’ ✔ checking re-building of vignette outputs (1.6s) ✔ checking for non-standard things in the check directory ✔ checking for detritus in the temp directory See ‘/private/var/folders/0x/x5wkbhmx0k74tncn9swz7xpr0000gn/T/RtmpYiEbLJ/file75936b43aefb/gap.Rcheck/00check.log’ for details.
R CMD check results
show/hide check() R CMD check
── R CMD check results ──────────────────────────────────── gap 0.0.0.9000 ──── Duration: 48.2s ❯ checking for code/documentation mismatches ... WARNING Data codoc mismatches from documentation object 'movies': Variables in data frame 'movies' Code: actor1 actor2 actor3 actor4 actor5 audience_rating audience_score best_actor_win best_actress_win best_dir_win best_pic_nom best_pic_win critics_rating critics_score director dvd_rel_date dvd_rel_day dvd_rel_month dvd_rel_year genre imdb_num_votes imdb_rating imdb_url mpaa_rating rt_url runtime studio thtr_rel_date thtr_rel_day thtr_rel_month thtr_rel_year title title_type top200_box Docs: actor1 actor2 actor3 actor4 actor5 audience_rating audience_score best_actor_win best_actress_win best_dir_win best_pic_nom best_pic_win critics_rating critics_score director dvd_rel_date dvd_rel_day dvd_rel_month dvd_rel_year genre imdb_num_votes imdb_rating imdb_url mpaa_rating rt_url runtime studio thtr_rel_date thtr_rel_day thtr_rel_year title title_type top200_box 0 errors ✔ | 1 warning ✖ | 0 notes ✔
rhub::check_for_cran()
: “runcheck_for_cran()
and assign the result to an object” … “use thecran_summary()
method to get a message that you can copy-paste in yourcran-comments.md
file” -rhub
Local
, CRAN
or
Package
Manager
devtools::build()
is also a regular part of the package development process. This function will source and bundle your package (learn the differences here).
# Deploy
## Local, CRAN or Package Manager ----
## This will build a tar.gz that can be installed locally,
## sent to CRAN, or to a package manager
::build() devtools
This will create a gap_0.0.0.9000.tar.gz
file to share or submit to a package management system.
── R CMD build ───────────────────────────────────────────────────────────────
✔ checking for file ‘/projects/apps/gap/DESCRIPTION’ ...
─ preparing ‘gap’: (613ms)
✔ checking DESCRIPTION meta-information ...
─ installing the package to build vignettes
✔ creating vignettes (6.6s)
─ excluding invalid files
Subdirectory 'R' contains invalid file names:
‘_disable_autoload.R’
─ checking for LF line-endings in source and make files and shell scripts
─ checking for empty or unneeded directories
─ building ‘gap_0.0.0.9000.tar.gz’
[1] "/projects/apps/gap_0.0.0.9000.tar.gz"
devtools::install()
is another common development workflow function.
RStudio
To deploy an application with RStudio (Posit) products, use of the functions below:
## RStudio ----
## If you want to deploy on RStudio related platforms
::add_rstudioconnect_file()
golem::add_shinyappsio_file()
golem::add_shinyserver_file() golem
Docker
If you use Docker to deploy applications, you can use the following functions:
show/hide docker functions
## Docker ----
## If you want to deploy via a generic Dockerfile
::add_dockerfile_with_renv()
golem
## If you want to deploy to ShinyProxy
::add_dockerfile_with_renv_shinyproxy() golem
Read more:
app.R
I’ll deploy my app using shinyapps.io, so after running golem::add_shinyappsio_file()
I will see the following output and a new app.R
file.
::add_shinyappsio_file() golem
── Creating _disable_autoload.R ──────────────────────────────────
✔ Created
✔ Setting active project to '/Users/mjfrigaard/projects/gap'
✔ Adding '^app\\.R$' to '.Rbuildignore'
✔ Adding '^rsconnect$' to '.Rbuildignore'
✔ Adding 'pkgload' to Imports field in DESCRIPTION
• Refer to functions with `pkgload::fun()`
✔ File created at /Users/mjfrigaard/projects/gap/app.R
To deploy, run:
• rsconnect::deployApp()
• Note that you'll need to upload the whole package to ShinyApps.io
- View the
app.R
file contents below:
# 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, helpers = FALSE, attach_testthat = FALSE)
pkgloadoptions( "golem.app.prod" = TRUE)
::run_app() # add parameters here (if any) gap
deployApp()
The rsconnect::deployApp()
function will deploy the application to the shinyapps.io site (provided you have an account).
::deployApp(appName = "gap-movies") rsconnect
Preparing to deploy application...DONE
Uploading bundle for application: 9468261...DONE
Deploying bundle: 7466839 for application: 9468261 ...
Waiting for task: 1319021708
building: Parsing manifest
building: Building image: 8845318
building: Installing system dependencies
building: Fetching packages
building: Building package: pkgload
building: Building package: testthat
building: Installing files
building: Pushing image: 8845318
deploying: Starting instances
rollforward: Activating new instances
success: Stopping old instances
Application successfully deployed to https://mjfrigaard.shinyapps.io/gap-movies/
End 03_deploy.R
View the deployed application here.
Recap
Building an application with golem
is very similar to developing an R package. The overall process remains the same: New code is placed in R/
, external resources are placed in inst/
, tests are stored and run from tests/testthat/
, etc. The figure below displays how the golem
framework works within the R package structure to create ‘production-grade shiny applications’:
golem
app-package
golem
framework overview
I didn’t cover using renv
with this app-package, but you can read more about this in the Using {renv
} section.