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:

install.packages("golem")
library(golem)
golem::create_golem(path = "gap")

If creating a golem app from RStudio’s New Project Wizard, the following defaults are available:

Figure 1: Creating a new golem shiny app

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
    ├── inst
       ├── app
       │   └── www
       │       └── favicon.ico
       └── golem-config.yml
    ├── man
       └── run_app.Rd
    └── gap.Rproj

The dev/ folder contains golem‘s ’guided tour’ scripts. These contain functions to help guide application development.

  • dev/01_start.R opens automatically
gap/dev/
    ├── 01_start.R
    ├── 02_dev.R
    ├── 03_deploy.R
    └── run_dev.R

1 directory, 4 files
  • dev/run_dev.R is for running the ‘development version’ of the application.

If you are familiar with R package development, you can think of the dev/ scripts as a ‘Shiny app-package development checklist.’

  • DESCRIPTION: In the dev/01_start.R script, users build a DESCRIPTION file with golem::fill_desc()

    • fill_desc() uses the desc package and the sections are entered in a key = "value" format
    golem::fill_desc(
      pkg_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)
    )
    • In dev/02_dev.R, the attachment::att_amend_desc() will “Amend DESCRIPTION with dependencies read from package code parsing”.
    attachment::att_amend_desc()
    • If attachment is not installed, use install.package('attachment')

dev/01_start.R contains the usethis functions for for creating common package development files:

  • LICENSE

    usethis::use_mit_license()
  • README

    usethis::use_readme_rmd()
  • Code of Conduct

    usethis::use_code_of_conduct()
  • Lifecycle badge

    usethis::use_lifecycle_badge("Experimental")
  • NEWS.md

    usethis::use_news_md(open = FALSE)
  • Git

    usethis::use_git()

golem files

The golem functions in dev/01_start.R are for setting the golem options and using recommended tests.

  • Options

    golem::set_golem_options()
  • Tests (with testthat)

    golem::use_recommended_tests()
  • Favicon

    golem::use_favicon()
  • Helper functions: golem::use_utils_ui() and golem::use_utils_server() create golem’s UI (R/golem_utils_ui.R) and server (R/golem_utils_server.R) utility functions in the R/ folder

golem gives away lots of free code!

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:

App Code

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.

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.

    #' The application User-Interface
    #'
    #' @param request Internal parameter for `{shiny}`.
    #'     DO NOT REMOVE.
    #' @noRd
    app_ui <- function(request) {
      tagList(
        # Leave this function for adding external resources
        golem_add_external_resources(),
        # Your application UI logic
        fluidPage(
          h1("gap")
        )
      )
    }
    #' The application server-side
    #'
    #' @param input,output,session Internal parameters for {shiny}.
    #'     DO NOT REMOVE.
    #' @import shiny
    #' @noRd
    app_server <- function(input, output, session) {
      # Your application server logic
    }
  • If you do some digging, you’ll find most of these golem utilities are wrappers for shiny and usethis functions. For example, golem_add_external_resources() is a wrapper for shiny::addResourcePath() and htmltools::htmlDependency():

    #' Add external Resources to the Application
    #'
    #' This function is internally used to add external
    #' resources inside the Shiny application.
    #'
    golem_add_external_resources <- function() {
      add_resource_path(
        "www",
        app_sys("app/www")
      )
    
      tags$head(
        favicon(),
        bundle_resources(
          path = app_sys("app/www"),
          app_title = "gap"
        )
        # Add here other external resources
        # for example, you can add shinyalert::useShinyalert()
      )
    }
    • And app_sys() is a wrapper for system.file():
    #' Access files in the current app
    #'
    #' NOTE: If you manually change your package name in the DESCRIPTION,
    #' don't forget to change it here too, and in the config file.
    #' For a safer name change mechanism, use the `golem::set_golem_name()` function.
    #'
    #' @param ... character vectors, specifying subdirectory and file(s)
    #' within your package. The default, none, returns the root of the app.
    #'
    #' @noRd
    app_sys <- function(...) {
      system.file(..., package = "gap")
    }
  • run_app.R is an exported function that is available for me to run my app after I’ve installed the package:

    library(gap)
    gap::run_app()

Creating code files

  • golem has wrappers for creating modules and helper functions in the R/ folder:

    ## Add modules ----
    ## Create a module infrastructure in R/
    golem::add_module(name = "name_of_module1", with_test = TRUE) 
    golem::add_module(name = "name_of_module2", with_test = TRUE) 
    
    ## Add helper functions ----
    ## Creates fct_* and utils_*
    golem::add_fct("helpers", with_test = TRUE)
    golem::add_utils("helpers", with_test = TRUE)
    • with_test = TRUE ensures these functions will also create test files in tests/

Configuration

  • The R/app_config.R file contains two functions: app_sys() (covered above) and get_golem_config(), which 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 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”
  • get_golem_config() is also included in the R/app_config.R file

    # Read App Config
    get_golem_config <- function(
      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")
    ) {
      config::get(
        value = value,
        config = config,
        file = file,
        use_parent = use_parent
      )
    }

Test files

golem::use_recommended_tests() creates the tests/ folder and a series of unit tests in the dev/01_start.R script. This function is essentially a wrapper around usethis::use_testthat(), but with some additional ‘recommendations’.2

  • golem::use_recommended_tests() adds the spelling package to our DESCRIPTION and updates the WORDLIST

  • The tests folder uses the testthat framework

    tests/testthat/
                ├── test-golem-recommended.R
                ├── test-golem_utils_server.R
                └── test-golem_utils_ui.R
    
    2 directories, 4 files

External files

The inst/ file initially has the following contents/structure:

inst/
  ├── WORDLIST
  ├── app/
     └── www/
         └── favicon.ico
  └── golem-config.yml 

The golem-config.yml file is covered above, but the inst/app/ folder works just like the inst/extdata folder (it is loaded when the package is installed and makes these files available to the application).

dev/02_dev.R includes golem wrappers for including CSS, JavaScript, and SASS files to the inst/app/www/ folder:

Deploy

The final script 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)

RStudio (Posit) Connect

Docker

  • golem::add_dockerfile_with_renv() and golem::add_dockerfile_with_renv_shinyproxy()

  • 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:

    golem::add_shinyappsio_file()
    ── 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
  • app.R contents

    # Launch the ShinyApp (Do not remove this comment)
    # To deploy, run: rsconnect::deployApp()
    # Or use the blue button on top of this file
    
    pkgload::load_all(export_all = FALSE, helpers = FALSE, attach_testthat = FALSE)
    options( "golem.app.prod" = TRUE)
    gap::run_app() # add parameters here (if any)

Building golem apps

Building an application with golem is very similar to developing an R package. However, golem streamlines some of the R package development processes into wrapper functions. The sections below cover creating modules, utility functions, and tests in a new golem app:

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 = 'name', 
           fct = 'fun', 
           utils = 'fun', 
           with_test = TRUE, 
           export = TRUE)

The code above creates the following files:

  • name = ‘name’: creates R/mod_name.R, a boilerplate Shiny module (ui and server functions)

    show/hide R/mod_name.R
    #' name UI Function
    #'
    #' @description A shiny Module.
    #'
    #' @param id,input,output,session Internal parameters for {shiny}.
    #'
    #' @rdname mod_name
    #' @export 
    #'
    #' @importFrom shiny NS tagList 
    mod_name_ui <- function(id){
      ns <- NS(id)
      tagList(
    
      )
    }
    
    #' name Server Functions
    #'
    #' @rdname mod_name
    #' @export 
    mod_name_server <- function(id){
      moduleServer( id, function(input, output, session){
        ns <- session$ns
    
      })
    }
    
    ## To be copied in the UI
    # mod_name_ui("name_1")
    
    ## To be copied in the server
    # mod_name_server("name_1")
  • fct = ‘fun’ and utils = ‘fun’: creates two empty R files in the R/ folder (R/mod_name_fct_fun.R and R/mod_name_utils_fun.R) with the same prefix as the module.

  • with_test = TRUE: creates tests/testthat/test-mod_name.R, a test file for the module with the following boilerplate tests:

    show/hide tests/testthat/test-mod_name.R
    testServer(mod_name_server,
      # Add here your module params
      args = list(), {
        ns <- session$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", {
      ui <- mod_name_ui(id = "test")
      golem::expect_shinytaglist(ui)
      # Check that formals have not been removed
      fmls <- formals(mod_name_ui)
      for (i in c("id")) {
        expect_true(i %in% names(fmls))
      }
    })
  • export = TRUE: exports the module functions (@export) with the name provided to name (@rdname).

The gap application includes two modules:

_apps/gap/R
    ├── mod_scatter_display.R
    └── mod_var_input.R

mod_var_input collects the reactive inputs from the UI and passes them to mod_scatter_display (view the code in these modules here on GitHub).

Utility functions

The scatter_plot() utility function was created with the utils argument of add_module(), so it’s stored the mod_scatter_display_utils.R file:

scatter_plot <- function(df, x_var, y_var, col_var, alpha_var, size_var) {
    ggplot2::ggplot(data = df,
      ggplot2::aes(x = .data[[x_var]],
          y = .data[[y_var]],
          color = .data[[col_var]])) +
      ggplot2::geom_point(alpha = alpha_var, size = size_var)

}

Utility functions can also be created directly with golem::add_utils() or golem::add_fct().3

Code file names

Including mod in the name of module scripts and functions makes it easier to separate them from other functions in my package namespace, if I’m using tab-completion, or if I’m searching for a particular file using Ctrl + .:

Figure 2: Go to File/Function in RStudio

UI

golem UI contents are placed in R/app_ui.R:

#' The application User-Interface
#'
#' @param request Internal parameter for `{shiny}`.
#'     DO NOT REMOVE.
#'
#' @keywords internal
app_ui <- function(request) {
  tagList(
    # Leave this function for adding external resources
    golem_add_external_resources(),
    # Your application UI logic
    fluidPage(
      sidebarLayout(
        sidebarPanel(
          mod_var_input_ui("vars")
        ),
        mainPanel(
          fluidRow(
            br(),
            p(em("Brought to you by: "),
              # add golem hex (in www/)
              img(src = "www/golem-hex.png", width = "5%")
            )
          ),
          mod_scatter_display_ui("plot")
        )
      )
    )
  )
}

Server

The module server functions are added to R/app_server.R:

#' The application server-side
#'
#' @param input,output,session Internal parameters for {shiny}.
#'     DO NOT REMOVE.
#'
#'
#' @keywords internal
app_server <- function(input, output, session) {
  # Your application server logic
   selected_vars <- mod_var_input_server("vars")
   
   mod_scatter_display_server("plot", var_inputs = selected_vars)
}

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
pth <- system.file('extdata/movies.RData', package = 'gap')
load(pth)
usethis::use_data(movies, overwrite = TRUE)

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.

Launch

golem has helper functions for adding the root app.R file to launch the application:

  • golem::add_rstudioconnect_file()

  • golem::add_shinyserver_file()

  • golem::add_shinyappsio_file()

The contents of the app.R file generated from golem::add_rstudioconnect_file() are below:

# Launch the ShinyApp (Do not remove this comment)
# To deploy, run: rsconnect::deployApp()
# Or use the blue button on top of this file

pkgload::load_all(export_all = FALSE, helpers = FALSE, attach_testthat = FALSE)
options( "golem.app.prod" = TRUE)
gap::run_app() # add parameters here (if any)

After running devtools::load_all(), devtools::document(), devtools::install(), the application can be launched from the app.R file (or by entering gap::run_app() in the Console).

(a) Initial gap::run_app()
Figure 3: The initial application in the gap package

Unit tests

golem apps come with boilerplate unit tests via use_recommended_tests(), use_utils_ui(with_test = TRUE), use_utils_server(with_test = TRUE).

tests/testthat/
            ├── test-golem-recommended.R
            ├── test-golem_utils_server.R
            └── test-golem_utils_ui.R

2 directories, 3 files

During development, we can include unit tests for new modules and/or functions using the with_test = TRUE argument.

System tests

System tests can be performed with shinytest2 (similar to non-package or non-golem apps). Two example shinytest2 tests can be found in test-shinytest2.R and test-app-feature-01.R:

tests
└── testthat
    ├── fixtures
       ├── make-tidy_ggp2_movies.R
       └── tidy_ggp2_movies.rds
    ├── helper.R
    ├── setup-shinytest2.R
    ├── test-app-feature-01.R
    └── test-shinytest2.R
  • test-shinytest2.R contains the boilerplate test from running shinytest2::record_test():

    show/hide test-shinytest2.R
    library(shinytest2)
    test_that("{shinytest2} recording: feature-01", {
      app <- AppDriver$new(name = "feature-01", height = 800, width = 1173)
      app$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()
    })
  • test-app-feature-01.R contains testthats BDD functions describing an app feature and scenario:

    show/hide test-app-feature-01.R
    library(shinytest2)
    describe("Feature 1: Scatter plot data visualization dropdowns
               As a film data analyst
               I want to explore variables in the movie review data
               So that I can analyze relationships between movie reivew sources", {
    
      describe("Scenario A: Change dropdown values for plotting
                 Given the movie review application is loaded
                 When I choose the variable [critics_score] for the x-axis
                 And I choose the variable [imdb_num_votes] for the y-axis
                 And I choose the variable [genre] for the color", {
            it("Then the scatter plot should show [critics_score] on the x-axis
                 And the scatter plot should show [imdb_num_votes] on the y-axis
                 And the points on the scatter plot should be colored by [genre]", {
                  app <- AppDriver$new(name = "feature-01-senario-a",
                                         height = 800, width = 1173)
                    app$set_inputs(`vars-y` = "imdb_num_votes")
                    app$set_inputs(`vars-x` = "critics_score")
                    app$set_inputs(`vars-z` = "genre")
                    app$expect_values()
           })
       })
    
      describe("Scenario B: Change dropdown values for plotting
                  Given the movie review application is loaded
                  When I choose the size of the points to be [3]
                  And I choose the opacity of the points to be [0.7]
                  And I enter '[New plot title]' for the plot title", {
             it("Then the size of the points on the scatter plot should be [3]
                  And the opacity of the points on the scatter plot should be [0.7]
                  And the title of the plot should be '[New plot title]'", {
                  app <- AppDriver$new(name = "feature-01-senario-b",
                                         height = 800, width = 1173)
                    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()
            })
        })
    })

Test coverage

The covrpage package provides a test coverage report in tests/README.md file. This file includes a report of the R file tested, the unit test context, number of tests, and the test status.4

Adding files and images

To include other files (like images), add the image file to inst/app/www/, then add the www/ to the path (see example UI code below)

# add icon
shiny::tags$img(src = "www/shiny.png")

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
golem::add_resource_path(
          prefix = '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
shiny::tags$img(src = "www/images/golem-hex.png")

In R/app_ui.R, the app_ui() function contains the UI layout functions (fluidPage(), sidebarLayout(), etc.), and a call to golem_add_external_resources():

#' The application User-Interface
#'
#' @param request Internal parameter for `{shiny}`.
#'     DO NOT REMOVE.
#' @import shiny
#' @noRd
app_ui <- function(request) {
  tagList(
    # Leave this function for adding external resources
    golem_add_external_resources(),
    # Your application UI logic
    fluidPage(
      sidebarLayout(
        sidebarPanel(
          mod_var_ui("vars"),
          h6(
            img(src = "www/images/shiny.png", width = "15%"),
            em(
              "The data for this application comes from the ",
              a("Building web applications with Shiny",
                href = "https://rstudio-education.github.io/shiny-course/"
              ),
              "tutorial"
            )
          )
        ),
        mainPanel(
          fluidRow(
            br(),
            p(em("Brought to you by: "),
              # add golem hex (in www/images/)
              img(src = "www/images/golem-hex.png", width = "5%")
            )
          ),
          mod_plot_ui("plot")
        )
      )
    )
  )
}

After running devtools::load_all(), devtools::document(), devtools::install(), the image is properly rendered with the application:

(a) gap::run_app()
Figure 4: Images in gap

golem takes advantage of the inst/ folder and R package structure to allow users to provide additional ‘assets’ to the application.

For example, if we use system.file() on the local directory ('.'), we see all the folders available to the application at runtime

fs::dir_tree(path = system.file('', package = 'gap'))
/Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/gap/.
├── DESCRIPTION
├── INDEX
├── LICENSE
├── Meta
│   ├── Rd.rds
│   ├── data.rds
│   ├── features.rds
│   ├── hsearch.rds
│   ├── links.rds
│   ├── nsInfo.rds
│   └── package.rds
├── NAMESPACE
├── R
│   ├── gap
│   ├── gap.rdb
│   └── gap.rdx
├── WORDLIST
├── app
│   └── www
│       ├── favicon.ico
│       └── images
│           ├── golem-hex.png
│           └── shiny.png
├── data
│   ├── Rdata.rdb
│   ├── Rdata.rds
│   └── Rdata.rdx
├── extdata
│   └── movies.RData
├── golem-config.yml
├── help
│   ├── AnIndex
│   ├── aliases.rds
│   ├── gap.rdb
│   ├── gap.rdx
│   └── paths.rds
└── html
    ├── 00Index.html
    └── R.css

Adding resources

golem has multiple functions for creating and using external files in your Shiny app-package. The add_dockerfile* set of functions are particularly helpful for including Docker files. I’ll cover two here: golem::add_dockerfile() and golem::add_dockerfile_with_renv().

In the gap directory, you’ll see the Dockerfile from add_dockerfile(). The files from add_dockerfile_with_renv() are in the inst/docker-renv folder..

add_dockerfile

add_dockerfile() results in the following Dockerfile:

FROM rocker/verse:4.3.2
RUN apt-get update && apt-get install -y  libicu-dev libxml2-dev make pandoc zlib1g-dev && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/
RUN echo "options(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 Rscript -e 'remotes::install_version("rlang",upgrade="never", version = "1.1.2")'
RUN Rscript -e 'remotes::install_version("knitr",upgrade="never", version = "1.45")'
RUN Rscript -e 'remotes::install_version("stringr",upgrade="never", version = "1.5.1")'
RUN Rscript -e 'remotes::install_version("shiny",upgrade="never", version = "1.8.0")'
RUN Rscript -e 'remotes::install_version("config",upgrade="never", version = "0.3.2")'
RUN Rscript -e 'remotes::install_version("spelling",upgrade="never", version = "2.2.1")'
RUN Rscript -e 'remotes::install_version("rmarkdown",upgrade="never", version = "2.25")'
RUN Rscript -e 'remotes::install_version("golem",upgrade="never", version = "0.4.1")'
RUN Rscript -e 'remotes::install_version("ggplot2",upgrade="never", version = "3.4.4")'
RUN Rscript -e 'remotes::install_github("rstudio/htmltools@a8a3559edbfd9dda78418251e69273fa9dfeb9bc")'
RUN Rscript -e 'remotes::install_github("r-lib/testthat@fe50a222c62cc8733b397690caf3b2a95856f902")'
RUN mkdir /build_zone
ADD . /build_zone
WORKDIR /build_zone
RUN R -e 'remotes::install_local(upgrade="never")'
RUN rm -rf /build_zone
EXPOSE 80
CMD R -e "options('shiny.port'=80,shiny.host='0.0.0.0');library(gap);gap::run_app()"

This Dockerfile sets up a Docker image with specific R/Linux packages, adds and installs the gap app-package from the current directory, and configures the container to run the Shiny app on port 80.

  1. FROM rocker/verse:4.3.2 which is a pre-built R environment.

  2. RUN apt-get update && apt-get install -y updates and installs several Linux packages:

    • libcurl4-openssl-dev: These are the development files and libraries for libcurl, which is used for for secure data transfer operations in R packages (the openssl indicates OpenSSL support).

    • libicu-dev: The development files and libraries for the International Components for Unicode library.

    • libssl-dev: The development files and libraries for SSL libraries, necessary for secure communications over networks.

    • libxml2-dev: Development files for the libxml2 library, which is used for parsing XML and HTML documents.

    • make: the make build utility that automatically builds executable programs and libraries from source code.

    • pandoc: the universal document converter used for converting markdown files to various other formats

    • zlib1g-dev: development files for the zlib compression library

    • rm -rf /var/lib/apt/lists/* cleans up the package list to reduce the image size.

  3. RUN mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/ creates two directories (/usr/local/lib/R/etc/ and /usr/lib/R/etc/) for R configuration files

  4. RUN echo "options(...) | tee ... configures global R settings (CRAN repository, download method, and number of CPU cores)

  5. RUN R -e 'install.packages("remotes")' installs the remotes package

  6. RUN Rscript -e 'remotes::install_version(...)' is used to installs specific versions of R packages used in the gap app-package without upgrading dependencies.

  7. RUN Rscript -e 'remotes::install_github(...)' is used to install R packages directly from GitHub repositories (at specific commits).

  8. RUN mkdir /build_zone will create a directory in the image for building the application.

  9. ADD . /build_zone adds the contents of the current directory (from where the Docker build command is run) into the /build_zone directory in the image.

  10. WORKDIR /build_zone sets the /build_zone directory as the working directory for all subsequent commands.

  11. RUN R -e 'remotes::install_local(upgrade="never")' installs the R package located in /build_zone without upgrading dependencies (this is our gap app-package being containerized).

  12. RUN rm -rf /build_zone reduce the final image size by removing the /build_zone directory.

  13. EXPOSE 80` exposes port 80 of the container (this is typically used for web applications like Shiny apps).

  14. CMD R -e "options('shiny.port'=80,shiny.host='0.0.0.0');library(gap);gap::run_app()" specifies the command to be run when the Docker container starts. These calls set the port and host Shiny app options, loads gap, and runs the standalone app function (gap::run_app()).

add_dockerfile_with_renv

add_dockerfile_with_renv() creates a tmp/deploy folder and adds the following files:

deploy/
  ├── Dockerfile
  ├── Dockerfile_base
  ├── README
  ├── gap_0.0.0.9000.tar.gz
  └── renv.lock.prod


Dockerfile_base is used to create a base image with necessary dependencies and configurations

FROM rocker/verse:4.3.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()'
  • Each instruction does the following:

    • RUN apt-get update -y && apt-get install updates and installs several Linux packages (make, zlib1g-dev, git, libicu-dev)

    • RUN mkdir and RUN echo creates directories and configures R settings (CRAN repository, download method, number of CPUs to use)

    • RUN R -e installs the remotes package and version 1.0.3 of the renv package (renv is used for project-local package management)

    • COPY copies a file renv.lock.prod into the image as renv.lock

    • RUN R -e uses renv::restore() to install the R packages specified in this lock file

Dockerfile builds on this base image to set up the gap app-package and its dependencies.

FROM gap_base
COPY renv.lock.prod renv.lock
RUN R -e '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
CMD R -e "options('shiny.port'=80,shiny.host='0.0.0.0');library(gap);gap::run_app()"
  • These instructions perform the following:
    • FROM gap_base starts from gap_base, which is the image built using Dockerfile_base.

    • COPY & RUN R -e repeats the step of copying renv.lock.prod and restoring packages using renv::restore().

    • COPY gap_*.tar.gz /app.tar.gz copies files matching gap_*.tar.gz (presumably our compressed app-package) into the image as /app.tar.gz.

    • RUN R -e installs this local package using remotes::install_local().

    • RUN rm reduce the size of the final image by removing the copied tar.gz file.

    • EXPOSE 80 exposes port 80 (necessary for external access to the app when it’s running inside the container).

    • CMD R -e defines the command to run when the container starts, which in this case, runs the gap application (gap::run_app()) on port 80 and (bound to 0.0.0.0).

The docker build and docker run commands in the README create the base image from Dockerfile_base, start the container from the Dockerfile image, and launche the gap application on port 80.

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 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:

pak::local_deps_explain(
  deps = 'golem', 
  root = "_apps/gap")
ℹ Loading metadata database
✔ Loading metadata database ... done

gap -> golem

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:

  1. dev/ contains golem‘s ’guided tour’ scripts (01_start.R, 02_dev.R, 03_deploy.R) and run_dev.R

  2. R/: the primary app files for the UI and server are stored in the R/ folder (R/app_ui.R, R/app_server.R, R/run_app.R), as well as the configuration function (R/app_config.R)

  3. golem apps are run using the gap::run_app() function (included in the R/ folder)

  4. While developing, golem also comes with a run_dev function that reads the R/run_dev.R file and evaluates the code.

  5. The inst/ folder holds the golem-config.yml and location of any external app files.

  6. 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
  • dev/01_start.R

  • dev/02_dev.R

  • dev/03_deploy.R

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)

name: ‘name’ creates mod_name_ui() and mod_name_server()

fct: creates R/mod_name_fct.R

utils: R/mod_name_utils.R

export: adds @export

with_test: creates tests/testthat/test-mod_name.R and includes boilerplate tests.

This is one of the best features in golem . A single function will create two module functions (and a module file), utility functions, and accompanying tests.

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:

  • add_js_file("script")

  • add_js_handler("handlers")

  • add_css_file("custom")

  • add_sass_file("custom")

Each add_ function has a template:

js_template()

js_hanler_template()

css_template()

sass_template()

Each of these functions create the necessary files in the inst/app folder.

Footnotes

  1. The code used to build the golem app is here.↩︎

  2. The tests golem creates in the tests/testthat/ folder can serve as a nice guide for users new to testthat↩︎

  3. New functions created with golem::add_*() functions are placed in the R/ folder with a @noRd tag by default (this behavior can be changed with the export argument).↩︎

  4. Test coverage is only included for the three initial golem test files (test-golem-recommended.R, test-golem_utils_server.R, test-golem_utils_ui.R).↩︎