9  External files


  • inst/: the inst/ folder is used to store files/resources that are available when your package is installed (i.e., ‘will be copied recursively to the installation directory.’)

  • system.file(): system.file() is a ‘file path accessor’ function

    • the directory structure of a source package is different from the directory structure of an installed package.

    • Use system.file() to access the installed version of your app-package

  • addResourcePath(): use addResourcePath() with system.file() and inst/ to include external files in your application.

Workflow: for a file located in inst/www/file.png:

# add path to app
addResourcePath('www', system.file('www', package = 'pkg'))
# use path without 'inst/' prefix in UI
img(src = 'www/shiny.png')

In this chapter we’ll cover how to add external resources (i.e., files previously stored and served from the www/ folder) to your app-package, and how to store and run multiple applications.

I’ve created the shinypak R package In an effort to make each section accessible and easy to follow:

Install shinypak using pak (or remotes):

# install.packages('pak')
pak::pak('mjfrigaard/shinypak')

Review the chapters in each section:

library(shinypak)
list_apps(regex = '^09')
## # A tibble: 4 × 2
##   branch          last_updated       
##   <chr>           <dttm>             
## 1 09.1_inst-www   2024-07-01 22:22:47
## 2 09.2_inst-bslib 2024-07-01 22:27:04
## 3 09.3_inst-dev   2024-07-01 22:37:26
## 4 09.4_inst-prod  2024-07-01 22:39:44

Launch an app with launch()

launch(app = "09.4_inst-prod")

Download an app with get_app()

get_app(app = "09.4_inst-prod")

9.1 External files

When we launch our app using the standalone app function, we see the following:

launch_app(run = 'p')

www/shiny.png is not accessible when we launch the app

The shiny.png logo in www/ is not being loaded into the UI when the application is launched. Serving the contents of www was previously being handled automatically by the Shiny framework, but now that moviesApp is an R package, we’ll need to explicitly tell the application where to find these resources.1

9.1.1 Package files

While developing, we are used to interacting with our app-package from the Files pane:

‘Source’ files moviesApp in Files pane

However, when we run install() (or use Ctrl/Cmd + Shift + B), the output in the Build pane gives us the location of our installed package:

==> R CMD INSTALL --preclean --no-multiarch --with-keep.source moviesApp

* installing to library ‘/path/to/installed/package/moviesApp/’
* installing *source* package ‘moviesApp’ ...
** using staged installation
** R
** data
*** moving datasets to lazyload DB
** inst
** byte-compile and prepare package for lazy loading
** help
*** installing help indices
** building package indices
** testing if installed package can be loaded from temporary location
** testing if installed package can be loaded from final location
** testing if installed package keeps a record of temporary installation path
* DONE 
1
This is the location of the installed version of moviesApp

Our app-package … installed

If we were to moviesApp to the path above, we’d see the files and folders in our installed package. Below are folder trees for our source package and our installed package:2

source/moviesApp/
├── DESCRIPTION
├── NAMESPACE
├── R
   ├── data.R
   ├── display_type.R
   ├── mod_scatter_display.R
   ├── mod_var_input.R
   ├── launch_app.R
   ├── movies_server.R
   ├── movies_ui.R
   └── scatter_plot.R
├── README.md
├── app.R
├── data
   ├── movies.RData
   └── movies.rda
├── inst
   └── extdata
       └── movies.fst
├── man
   ├── display_type.Rd
   ├── mod_scatter_display_server.Rd
   ├── mod_scatter_display_ui.Rd
   ├── mod_var_input_server.Rd
   ├── mod_var_input_ui.Rd
   ├── movies.Rd
   ├── launch_app.Rd
   ├── movies_server.Rd
   ├── movies_ui.Rd
   └── scatter_plot.Rd
├── moviesApp.Rproj
└── www
    └── shiny.png
installed/moviesApp/
├── DESCRIPTION
├── INDEX
├── Meta
   ├── Rd.rds
   ├── data.rds
   ├── features.rds
   ├── hsearch.rds
   ├── links.rds
   ├── nsInfo.rds
   └── package.rds
├── NAMESPACE
├── R
   ├── moviesApp
   ├── moviesApp.rdb
   └── moviesApp.rdx
├── data
   ├── Rdata.rdb
   ├── Rdata.rds
   └── Rdata.rdx
├── extdata
   └── movies.fst
├── help
   ├── AnIndex
   ├── aliases.rds
   ├── moviesApp.rdb
   ├── moviesApp.rdx
   └── paths.rds
└── html
    ├── 00Index.html
    └── R.css

The installed version of moviesApp has many of the same files as the ‘source’ version we’ve been working with (i.e., NAMESPACE and DESCRIPTION). It also might surprise you to see that many of the source files aren’t included in the installed version of moviesApp (.R, .Rd files. etc.).

Viewing the contents of your installed package should help demystify what happens when we run devtools::install() and give you a better idea of how system.file() works.3

The contents of the inst/ subdirectory will be copied recursively to the installation directory. Subdirectories of inst/ should not interfere with those used by R (currently, R/, data/, demo/, exec/, libs/, man/, help/, html/ and Meta/, and earlier versions used latex/, R-ex/).” - Writing R extensions, Package subdirectories

9.1.2 system.file()

system.file() gives us access to the package files on installation (i.e., the files we saw in the folder tree above). In the data chapter, we used system.file() to access the movies.fst file in inst/extdata/:

fst::read_fst(
  path = system.file("extdata/", "movies.fst", 
                     package = "moviesApp")
  )

As we can see, the movies.fst has two locations: the ‘source’ package location, and the ‘installed’ location.

Source package files

inst/
  └── extdata/
        └── movies.fst

Installed package files

└── extdata/
      └── movies.fst

system.file() is accessing movies.fst from the installed location.4

To include the contents of www/ in our app-package, we’ll need to move www/ into inst/, then access it’s contents with system.file() and addResourcePath().

9.1.3 addResourcePath()

The addResourcePath() function will add a “directory of static resources to Shiny’s web server.” In moviesApp, want to add the www directory that includes the shiny.png file.5

Current www location

├── inst
   └── extdata
       └── movies.fst
└── www
    └── shiny.png

 

New www location

inst/
  ├── extdata/
     └── movies.fst
  └── www/
      └── shiny.png

In R/movies_ui.R function, we’ll include the addResourcePath() at the top of the tagList() and reference the image in img() using only the subfolder in the path:

movies_ui <- function() {
  addResourcePath(
    prefix = 'www',
    directoryPath = system.file('www', package = 'moviesApp'))
  tagList(
    fluidPage(
      theme = shinythemes::shinytheme("spacelab"),
      titlePanel(
        div(
          img(
            src = "www/shiny.png",
            height = 60,
            width = 55,
            style = "margin:10px 10px"
            ), 
         "Movie Reviews"
        )
      ),
      sidebarLayout(
        sidebarPanel(
          mod_var_input_ui("vars")
        ),
        mainPanel(
          mod_scatter_display_ui("plot")
        )
      )
    )
  )
} 
1
Prefix (or folder name) of installed location
2
Path to installed package files
3
Reference to installed package image file

After loading, documenting, and installing, the application now includes the image file.



Ctrl/Cmd + Shift + L / D / B

Launch app with the shinypak package:

launch('09.1_inst-www')
library(moviesApp)
launch_app(run = 'p')

inst/www accessible with addResourcePath()

The inst/ folder can also be used to store files we’re using in alternate versions of our application. This can include alternate images, CSS styling, JS scripts, data files, or even entirely different apps!

9.2 bslib themes

We can use inst/ to store alternative files and configure our UI function to test different layouts. In the example below, I’ve included a second optional UI layout that uses a bslib theme in movies_ui().

Launch app with the shinypak package:

launch('09.2_inst-bslib')

The bslib argument includes an alternate image file (stored in inst/www/bootstrap.png):

Remember to include the bslib and sass packages to your DESCRIPTION with usethis::use_package().

show/hide movies_ui()
movies_ui <- function(bslib = FALSE) {
  addResourcePath(
    prefix = 'www', 
    directoryPath = system.file('www', package = 'moviesApp'))
  if (isFALSE(bslib)) {
  tagList(
    bslib::page_fillable(
      h1("Movie Reviews"),
      bslib::layout_sidebar(
        sidebar =
          bslib::sidebar(
            title = tags$h4("Sidebar inputs"),
            img(
              src = "shiny.png",
              height = 60,
              width = 55,
              style = "margin:10px 10px"
            ),
            mod_var_input_ui("vars")
          ),
        bslib::card(
          full_screen = TRUE,
          bslib::card_header(
            tags$h4("Scatter Plot")
          ),
          bslib::card_body(fillable = TRUE,
            mod_scatter_display_ui("plot")
          ),
          bslib::card_footer(
            tags$blockquote(
              tags$em(
                tags$p(
                  "The data for this application comes from the ",
                  tags$a("Building web applications with Shiny",
                    href = "https://rstudio-education.github.io/shiny-course/"
                  ),
                  "tutorial"
                )
              )
            )
          )
        )
      )
    )
  )
  } else {
    tagList(
      bslib::page_fillable(
        title = "Movie Reviews (bslib)",
        theme = bslib::bs_theme(
          bg = "#101010",
          fg = "#F6F5F5",
          primary = "#EE6F57",
          secondary = "#32E0C4",
          success = "#FF4B5C",
          base_font = sass::font_google("Ubuntu"),
          heading_font = sass::font_google("Ubuntu")
        ),
        bslib::layout_sidebar(
          sidebar = bslib::sidebar(
            open = TRUE,
            mod_var_input_ui("vars")
          ),
          bslib::card(
            full_screen = TRUE,
                bslib::card_header(
                  img(src = "www/bootstrap.png",
                  height = 80,
                  width = 100,
                  style = "margin:10px 10px")
              ),
             bslib::card_body(fillable = TRUE,
                 mod_scatter_display_ui("plot")
            )
          )
        )
      )
    )
  }
} 
1
Include inst/www resources
2
Standard fluidPage()
3
bslib layout
4
Reference to alternate image (bootstrap.png)



Ctrl/Cmd + Shift + L / D / B

This alternate version of launch_app() uses the same modules and utility functions as the previous versions, but when bslib = TRUE, the app displays the alternate UI layout:

launch_app(run = 'p', bslib = TRUE)
(a) launch_app() with logo
Figure 9.1: inst/www/bootstrap.png image from movies_ui()

The example above was a simple, but using inst/ to hold alternate features (i.e., images or custom .html, .css, .sass, .js code) that can be easily displayed with an optional argument is a great tool for demonstrating features to users and stakeholders.

9.3 dev data app

It’s not uncommon to be working on multiple ‘development’ versions of an application in the same package. In these situations, we might want to store the development files in the inst/dev folder.

Launch app with the shinypak package:

launch('09.3_inst-dev')

9.3.1 tidy_movies data

The ‘development’ application in moviesApp uses a tidy version of the ggplot2movies::movies data, which we created in the data-raw/tidy_movies.R file.6

show/hide data-raw/tidy_movies.R
## code to prepare the `tidy_movies` dataset goes here
# load packages --------------------
library(fst)

make_tidy_ggp2_movies <- function(movies_data_url) {
  movies_data <- read.csv(file = movies_data_url)
  # specify genre columns
  genre_cols <- c(
    "Action", "Animation",
    "Comedy", "Drama",
    "Documentary", "Romance",
    "Short"
  )
  # calculate row sum for genres
  movies_data$genre_count <- rowSums(movies_data[, genre_cols])
  # create aggregate 'genres' for multiple categories
  movies_data$genres <- apply(
    X = movies_data[, genre_cols],
    MARGIN = 1,
    FUN = function(row) {
      genres <- names(row[row == 1])
      if (length(genres) > 0) {
        return(paste(genres, collapse = ", "))
      } else {
        return(NA)
      }
    }
  )
  # format variables
  movies_data$genre_count <- as.integer(movies_data$genre_count)
  movies_data$genre <- ifelse(test = movies_data$genre_count > 1,
    yes = "Multiple genres",
    no = movies_data$genres
  )
  movies_data$genre <- as.factor(movies_data$genre)
  movies_data$mpaa <- factor(movies_data$mpaa,
    levels = c("G", "PG", "PG-13", "R", "NC-17"),
    labels = c("G", "PG", "PG-13", "R", "NC-17")
  )

  # reduce columns to only those in graph
  movies_data[, c(
    "title", "year", "length", "budget",
    "rating", "votes", "mpaa", "genre_count",
    "genres", "genre"
  )]
}

tidy_movies <- make_tidy_ggp2_movies("https://raw.githubusercontent.com/hadley/ggplot2movies/master/data-raw/movies.csv")

# save to inst/dev/
fst::write_fst(x = tidy_movies, path = "inst/dev/tidy_movies.fst")

9.3.2 inst/dev/R

We can place the application modules, UI, and server functions in inst/dev/R. Both functions in the dev/ display module has been re-written to add functionality for importing the tidy_movies.fst data file and an option to removing missing values from the graph.

Any of the functions from moviesApp can be used in the dev/ modules with explicit namespacing (i.e., moviesApp::fun()). For example, dev_mod_vars_ui() contains choices for the columns in the tidy_movies data, but there’s no need to re-write the mod_var_input_server() function.7

The app.R file will contain the call to shinyApp(), and any other packages we’ll need to launch the application. The data and alternative image file can be placed in the root folder (with the app.R file):

inst/dev
├── R
   ├── devServer.R
   ├── devUI.R
   ├── dev_mod_scatter.R
   └── dev_mod_vars.R
├── app.R
├── imdb.png
└── tidy_movies.fst

2 directories, 7 files

9.3.3 Launch dev

Finally, the development app is launched with it’s own standalone function (stored in R/ggp2_launch_app.R). The ggp2_launch_app() function is similar to launch_app(), but appDir is set to the location of the development files (which we provide with system.file()).

Launch app with the shinypak package:

launch('09.3_inst-dev')
show/hide R/ggp2_launch_app.R
#' Development `ggplot2movies` app standalone function
#'
#' Wrapper function for `shinyAppDir()`
#' 
#' @param test logical, run in `test.mode`? Defaults to `TRUE`.
#' 
#' @return shiny app
#' 
#'
#' @export
ggp2_launch_app <- function(options = list(), run = "w") {
  if (interactive()) {
    display_type(run = run)
  } 
    shinyAppDir(
    appDir = system.file("dev",
      package = "moviesApp"
    ),
    options = options
  )
}


After loading, documenting, and installing moviesApp, we can run the development version using ggplot2_launch_app():



Ctrl/Cmd + Shift + L / D / B

ggp2_launch_app(run = 'p')
(a) ggplot2_launch_app()
Figure 9.2: inst/dev/ app with dev_movies_ui()

You may have noticed that I’ve used a different color and theme for the two development examples above. I’ve found this can be a quick and easy way to differentiate ‘development’ and ‘production’ versions of an application.

9.4 Production (prod)

It’s also possible to have a folder dedicated for deploying your application in your app-package.

Launch app with the shinypak package:

launch('09.4_inst-prod')

9.4.1 prod data

This folder can be named something like inst/prod/ or inst/deploy, and it will contain the ‘production’ ready versions of UI and server functions in a single app.R file:

inst/
  └── prod/
      └── app
          └── app.R
          
2 directories, 1 file

9.4.2 prod/app/app.R

In the app.R file, include only a call to shinyApp() with the ui and server function (explicitly namespaced from your app-package):

show/hide prod/app/app.R
shinyApp(
  ui = moviesApp::movies_ui(bslib = TRUE), 
  server = moviesApp::movies_server)

I used the bslib version, just to differentiate it from the other applications in moviesApp.

9.4.3 Deploying prod

Back in the root app.R file, we’ll use shinyAppDir() and system.file() to return the app object from prod/app/app.R:

show/hide app.R
# set option to turn off loadSupport() ----
withr::with_options(new = list(shiny.autoload.r = FALSE), code = {
  if (!interactive()) {
    sink(stderr(), type = "output")
    tryCatch(
      expr = {
        # load package ----
        library(moviesApp)
      },
      error = function(e) {
        # load R/ folder ----
        pkgload::load_all()
      }
    )
    # create shiny object from prod/app ----
    shinyAppDir(appDir = 
        system.file("prod/app", package = "moviesApp"))
  } else {
    # load R/ folder ----
    pkgload::load_all()
    # create shiny object ----
    shiny::shinyApp(
      ui = movies_ui,
      server = movies_server
    )
  }
})



Ctrl/Cmd + Shift + L / D / B

To deploy the app, call rsconnect::deployApp() in the console.

rsconnect::deployApp(appName = 'moviesApp-launch')

The deployment log will look something like this:

── Preparing for deployment ────────────────────────────────────────────
✔ Deploying "moviesApp-launch" to "server: shinyapps.io / username: yourusername"
ℹ Bundling 35 files: .Rbuildignore, app.R, data/movies.rda, data/movies.RData,
  data-raw/tidy_movies.R, DESCRIPTION, inst/dev/app.R, inst/dev/imdb.png, 
  inst/dev/tidy_movies.fst, inst/extdata/movies.fst, inst/prod/app/app.R, 
  inst/www/bootstrap.png, inst/www/shiny.png, man/display_type.Rd, 
  man/ggp2_launch_app.Rd, man/mod_scatter_display_server.Rd, 
  man/mod_scatter_display_ui.Rd, man/mod_var_input_server.Rd, …, 
  R/scatter_plot.R, and README.md
ℹ Capturing R dependencies with renv
✔ Found 72 dependencies
✔ Created 1,179,590b bundle
ℹ Uploading bundle...
✔ Uploaded bundle with id 7749936
── Deploying to server ────────────────────────────────────────────────────────────────────
Waiting for task: 1341341295
  building: Parsing manifest
  building: Building image: 9226179
  building: Fetching packages
  building: Installing packages
  building: Installing files
  building: Pushing image: 9226179
  deploying: Starting instances
── Deployment complete ───────────────────────────────────────────
✔ Successfully deployed to <https://mjfrigaard.shinyapps.io/moviesApp-launch/>

You can see a deployed version of this application here

You can explore the structure of other installed packages to see how they work ‘under the hood’ to gain insight into how they use the inst/ folder.

  • For example, the inst/extdata/ folder in the readr package holds a variety of datasets:

    /path/to/install/Library/R/x86_64/4.2/library/readr/
    
    extdata/
      ├── challenge.csv
      ├── chickens.csv
      ├── epa78.txt
      ├── example.log
      ├── fwf-sample.txt
      ├── massey-rating.txt
      ├── mini-gapminder-africa.csv
      ├── mini-gapminder-americas.csv
      ├── mini-gapminder-asia.csv
      ├── mini-gapminder-europe.csv
      ├── mini-gapminder-oceania.csv
      ├── mtcars.csv
      ├── mtcars.csv.bz2
      ├── mtcars.csv.zip
      └── whitespace-sample.txt
    
    1 directory, 15 files
  • These files are used in readr::readr_example()):

    #' Get path to readr example
    #'
    #' readr comes bundled with a number of sample files in its `inst/extdata`
    #' directory. This function make them easy to access
    #'
    #' @param file Name of file. If `NULL`, the example files will be listed.
    #' @export
    #' @examples
    #' readr_example()
    #' readr_example('challenge.csv')
    readr_example <- function(file = NULL) {
      if (is.null(file)) {
        dir(system.file('extdata', package = 'readr'))
      } else {
        system.file('extdata', file, package = 'readr', mustWork = TRUE)
      }
    }

Recap

This chapter had covered how to include external files and resources (i.e., what was previously stored in the www/ folder of a regular Shiny app project) in your app-package with addResourcePath() and system.file().

We’ve also covered how to use the inst/ folder to include alternative files, development and production/deployment versions of your app. You can now launch the following applications from moviesApp:

Standard application with/without test mode

library(moviesApp)
launch_app(options = list(test.mode = TRUE))
# or 
launch_app(options = list(test.mode = FALSE))

blisb application with/without test mode

library(moviesApp)
launch_app(options = list(test.mode = TRUE), bslib = TRUE)
# or 
launch_app(options = list(test.mode = FALSE), bslib = TRUE) 

ggplot2movies data application with/without test mode

library(moviesApp)
ggp2_launch_app(options = list(test.mode = TRUE))
# or 
ggp2_launch_app(options = list(test.mode = FALSE))

prod/ application

library(moviesApp)
rsconnect::deployApp()

In the next chapter, we’re going to cover testing the code in your shiny app-package.

Recap: inst & www folders
  • inst/: the inst/ folder is installed with your app-package and will be accessible to users, so it’s a great location for files you want contained in your app, but don’t fit into the standard R package structure.

    • inst/ is also a great location for alternative versions of applications (i.e., inst/app/dev or inst/app/prod/).
  • system.file(): constructs a path to files or folders within installed packages and is especially useful when working with external datasets (i.e., inst/extdata/) or other external resources included with your app-package (i.e., inst/www/).

  • www: used for external static resources in shiny apps. shiny will automatically serve files under the www/ directory, but in app-packages we need to explicitly set this location with shiny::addResourcePath()

  • addResourcePath(): create a prefix (i.e., path) for a directoryPath of static files to accessible in shiny’s web server:

    # file location
    inst/
      └── www/
            └── shiny.png
    # add path to app 
    addResourcePath(prefix = 'www', 
                    directoryPath = system.file('www', 
                                    package = 'moviesApp'))
    # use path without 'inst/' prefix
    shiny::img(src = 'www/shiny.png')

Please open an issue on GitHub


  1. This is a common problem developers encounter when converting shiny app into app-packages. See this popular thread on Posit Community.↩︎

  2. fs::path_package(package = "moviesApp") returns the path to your installed package and fs::dir_tree() function will print a folder tree.↩︎

  3. Read more about sub-directories to avoid in inst/ in R Packages, 2ed.↩︎

  4. The key takeaway here is that the inst/ subfolders and files are available unchanged in the installed version (with the inst/ folder omitted.).↩︎

  5. You can read more about adding external resources in the documentation for addResourcePath().↩︎

  6. We covered the data-raw/ folder in the Data chapter, and you can read more about it here in R packages, 2ed↩︎

  7. This requires exporting mod_var_input_server() with @export in the R/ folder.↩︎