4  Development

Published

2024-09-19


Package development involves three habits:

  1. Loading the code in the R/ folder:

    Ctrl/Cmd + Shift + L / devtools::load_all()

  1. Creating the NAMESPACE and help files in the man/ folder:

    Ctrl/Cmd + Shift + D / devtools::document()

  1. Installing the package :

    Ctrl/Cmd + Shift + B / devtools::install()


After creating a DESCRIPTION file with the mandatory fields, moving the .R files into the R/ folder, and configuring the project build tools in .Rproj, we’re ready to test our app-package functionality with the devtools package.

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 = '^04')
## # A tibble: 1 × 2
##   branch      last_updated       
##   <chr>       <dttm>             
## 1 04_devtools 2024-09-03 22:01:57

Launch the app:

launch("04_devtools")

Download the app:

get("04_devtools")

By ‘functionality’, I mean our app-package can call the devtools functions for loading the code in R/, creating documentation, and successfully installing the package from the source files.

If you’d like a refresher on the Shiny and R package chapters, I’ve provided a refresher of these topics below:

Shiny apps:

  • R/ folder: converting the application code into functions (i.e., modules and a standalone app function) and placing them alongside any utility functions in an R/ folder removes the need to call source() in app.R.

  • www/ folder: images, CSS, JavaScript, and other static resources can be stored in www/ and Shiny will serve these files when the application is run.

R Packages:

  • R packages require a DESCRIPTION file with the following fields:

    • Package, Version, License, Description, Title, Author, and Maintainer.
  • usethis::create_package() will create new app-packages, and can be used to convert existing Shiny app projects into Shiny app-packages.

  • The Build pane in the IDE requires the package build fields in the .Rproj file

    • The package development settings can be accessed via Tools > Project Options… > Build Tools

4.1 Package dev with devtools

If you’re new to package development, having a little background on the devtools package is helpful. Earlier versions of devtools contained most of the functions used for package development. In version 2.0, devtools went under a conscious uncoupling, which means there was a “division of labor” for its core functionality:

  • The usethis package contains the functions for creating package folders and files (.R files, tests, vignettes, etc.). usethis is also automatically loaded when you call library(devtools).

  • Loading and building your app-package is handled by pkgload and pkgbuild

  • For app-packages destined for CRAN, the R CMD check is handled by rcmdcheck and revdepcheck

  • Installing packages from non-CRAN repositories (i.e., install_github()) is handled by remotes

You don’t have to install all of these packages (they will be loaded with devtools), but the information is essential because it affects the dependencies in your app-package:

Package developers who wish to depend on devtools features should also pay attention to which package the functionality is coming from and depend on that rather than devtools. In most cases, packages should not depend on devtools directly.’ - devtools 2.0.0, tidyverse blog

We will cover this topic more in the dependencies chapter..

Let’s assume we’re continuing with the app project we converted manually in the previous branch of sap (the files and folders are below).

See the 03.1_description branch of sap.

sap/ 
  ├── DESCRIPTION
  ├── R
     ├── mod_scatter_display.R
     ├── mod_var_input.R
     └── utils.R
  ├── README.md
  ├── app.R
  ├── man
  ├── movies.RData
  ├── sap.Rproj
  └── www
      └── shiny.png

4 directories, 9 files

We’ll revert to the previous version of sap we created by manually editing the DESCRIPTION file1 to show the connection between the devtools functions and specific fields in the DESCRIPTION file.2

The DESCRIPTION file

The version of sap in this branch has a DESCRIPTION file with the seven mandatory fields:

Package: sap
Version: 0.0.0.9000
Type: Package
Title: Shiny App-Packages
Description: An R package with a collection of Shiny applications.
Author: John Smith [aut, cre]
Maintainer: John Smith <John.Smith@email.io>
License: GPL-3
                                                        

Leave an empty final line in the DESCRIPTION

The .Rproj file

However, the .Rproj file is still configured to work with a Shiny project:3

Version: 1.0

RestoreWorkspace: Default
SaveWorkspace: Default
AlwaysSaveHistory: Default

EnableCodeIndexing: Yes
UseSpacesForTab: Yes
NumSpacesForTab: 2
Encoding: UTF-8

RnwWeave: Sweave
LaTeX: XeLaTeX

The differences between developing an R package and a Shiny app can be boiled down to a handful habits, each of which calls a devtools function:

I’ll use this font style to indicate each devtools habit and accompanying function.

  1. Load all the functions and data in your app-package with load_all()

  2. Document the app-package functions and data with document()

  3. Install the app-package with install()

In the sections below, I’ll cover each function and some opinions about how they should be used when your Shiny app transitions to an app-package.4

Before we can start developing, we need to install devtools:

install.packages("devtools")
library(devtools)

usethis is automatically loaded/attached with devtools.

Loading required package: usethis
Keyboard shortcuts

I strongly recommend using the keyboard shortcuts for each devtools function. Shortcuts reduce typing and bundle all those keystrokes into a single action. They also create a kind of ‘muscle memory’ for each step.

In RStudio , new keyboard shortcuts can be created using the shrtcts package or by clicking on Tools > Modify Keyboard Shortcuts.

In Positron , the devtools functions covered below are already mapped to the keyboard shortcuts. Follow the instructions found in Positron’s Wiki to add new shortcuts.

4.2 Load

load_all() is the most common devtools function we’ll use during development because we should load the package when anything changes in the R/ folder.

load_all() removes friction from the development workflow and eliminates the temptation to use workarounds that often lead to mistakes around namespace and dependency management’ - Benefits of load_all(), R Packages, 2ed


Ctrl/Cmd + Shift + L

=

devtools::load_all()


Using load_all() is similar to calling library(sap) because it loads the code in R/ along with any data files. load_all() is also designed for iteration (unlike using source()), and when it’s successful, the output is a single informative message:

ℹ Loading sap

4.3 Document

The document() function from devtools serves two purposes:

  1. Writing the package NAMESPACE file

  2. Creates the help files in the man/ folder

Document the package whenever changes are made to any roxygen2 syntax (or settings).


Ctrl/Cmd + Shift + D

=

devtools::document()


devtools is smart enough to recognize the first time document() is called, so when I initially run it in the Console, it prompts me that the roxygen2 version needs to be set in the DESCRIPTION file:5

ℹ Updating sap documentation
First time using roxygen2. Upgrading automatically...
Setting `RoxygenNote` to "7.3.2"

You may have noticed calling document() also calls load_all(), which scans the loaded package contents for special documentation syntax before writing the NAMESPACE file (we’ll cover the NAMESPACE in the chapter on Dependencies).

ℹ Loading sap
Writing NAMESPACE

If we open the NAMESPACE file, we see it’s empty (and that we shouldn’t edit this file by hand).

Figure 4.1: Initial NAMESPACE file

The last few output lines warn us to include the Encoding field in the DESCRIPTION.

Warning message:
roxygen2 requires Encoding: "UTF-8"
ℹ Current encoding is NA 

devtools won’t automatically add Encoding (like it did with RoxygenNote above), so we’ll need to add it to the DESCRIPTION file manually:

Always leave an empty final line in the DESCRIPTION

Package: sap
Version: 0.0.0.9000
Type: Package
Title: movies app
Description: A movies data Shiny application.
Author: John Smith [aut, cre]
Maintainer: John Smith <John.Smith@email.io>
License: GPL-3
RoxygenNote: 7.2.3
Encoding: UTF-8
1
The Encoding value shouldn’t include quotes like the warning message above (i.e., UTF-8)

After adding the required fields to the DESCRIPTION file,6 we’ll document() the package again using the keyboard shortcut:

In the Build pane, we see the following:

==> devtools::document(roclets = c('rd', 'collate', 'namespace'))

ℹ Updating sap documentation
ℹ Loading sap
Documentation completed

4.4 Install

At the time of this writing, the 2024.09.0-1 pre-release of Positron was available for testing.

The final development habit checking if our app-package can be installed locally with devtools::install() or pak::local_install(upgrade = FALSE) (depending on the IDE you’re using).

Install a package after the initial setup, after major changes to the code, documentation, or dependencies, and before committing or sharing.


Ctrl/Cmd + Shift + B

=

devtools::install()


install() will prompt the following output in the Build pane:

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

* installing to library ‘/path/to/local/install/sap-090c61fc/R-4.2/x86_64-apple-darwin17.0’
* installing *source* package ‘sap’ ...
** using staged installation
** R
** byte-compile and prepare package for lazy loading
No man pages found in package  ‘sap’
** 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 (sap)
1
We saw both of these R CMD INSTALL settings in the sap.Rproj file from the previous chapter
2
Full file path for installation
3
install() attempts to install the package from the *source* files and a ‘bundle’ or source tarball file (i.e., .tar.gz)
4
No man pages found in package 'sap' tells us none of the code in R/ has adequately been documented (which we’ll cover in the roxygen2 chapter)
5
Building the ?help files
6
Checks to see if package can be loaded from multiple locations and stores
7
Checks to see if package stores the install location
8
DONE (sap) means sap was successfully installed!


In Positron, Ctrl/Cmd + Shift + B will call pak::local_install(upgrade = FALSE). This command will be run in a new Terminal window:

 *  Executing task: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/bin/R -e 'pak::local_install(upgrade = FALSE)'


R version 4.4.0 (2024-04-24) -- "Puppy Cup"
Copyright (C) 2024 The R Foundation for Statistical Computing
Platform: x86_64-apple-darwin20

R is free software and comes with ABSOLUTELY NO WARRANTY.
You are welcome to redistribute it under certain conditions.
Type 'license()' or 'licence()' for distribution details.

  Natural language support but running in an English locale

R is a collaborative project with many contributors.
Type 'contributors()' for more information and
'citation()' on how to cite R or R packages in publications.

Type 'demo()' for some demos, 'help()' for on-line help, or
'help.start()' for an HTML browser interface to help.
Type 'q()' to quit R.

> pak::local_install(upgrade = FALSE)
✔ Updated metadata database: 7.50 MB in 12 files.
✔ Updating metadata database ... done
 
→ Will update 1 package.
→ The package (0 B) is cached.
+ sap 0.0.0.9000 → 0.0.0.9000 👷🏾‍♂️
ℹ No downloads are needed, 1 pkg is cached
✔ Got sap 0.0.0.9000 (source) (96 B)
ℹ Packaging sap 0.0.0.9000
✔ Packaged sap 0.0.0.9000 (18.2s)
ℹ Building sap 0.0.0.9000
✔ Built sap 0.0.0.9000 (3.1s)
✔ Installed sap 0.0.0.9000 (local) (63ms)
✔ 1 pkg + 54 deps: kept 54, upd 1, dld 1 (NA B) [53.2s]
> 
> 
 *  Terminal will be reused by tasks, press any key to close it.
1
Name of task and terminal
2
Starts new R session
3
Calls pak::local_install(upgrade = FALSE)
4
pak will check the package database for updates
5
the upgrade = FALSE means pak is going to do “the minimum amount of work to give you the latest version(s) of pkg
6
Packaging sap
7
Building sap
8
Installing sap
9
Summary (‘kept 54 dependencies, updated 1, downloaded 1 package’)
10
Close Terminal message


What’s the difference?

devtools::install() focuses on helping package developers by managing all necessary steps for installation, including rebuilding documentation and running tests. devtools::install() also automatically updates outdated dependencies during installation unless dependencies is set to FALSE.

pak::local_install() is designed to use parallel downloads and more efficient dependency resolution, making faster and more reliable than devtools in many cases.7 The upgrade = FALSE installs a package without upgrading its dependencies, keeping the current package versions intact.

Launch app with the shinypak package:

launch('04_devtools')

4.5 Check?

devtools::check() performs a series of checks to ensure a package meets the standards set by CRAN. You can consider check() as a ‘quality control’ function for documentation, NAMESPACE dependencies, unnecessary or non-standard folders and files, etc. R Packages recommends using check() often, but I agree with the advice in Mastering Shiny on using check() with app-packages,

‘I don’t recommend that you [call devtools::check()] the first time, the second time, or even the third time you try out the package structure. Instead, I recommend that you get familiar with the basic structure and workflow before you take the next step to make a fully compliant package.’

However, I’ve included an example of running check() on sap in the callout box below to demonstrate how it works.

devtools::check()

The output from check() can be rather lengthy (it’s pretty comprehensive!), and it provides feedback on each item in the form of a note (N), warning (W), or error (E).

==> devtools::check()

Duration: 15.3s

N  checking top-level files
   Non-standard files/directories found at top level:
     ‘app.R’ ‘movies.RData’

W  checking dependencies in R code ...
   '::' or ':::' imports not declared from:
     ‘ggplot2’ ‘shiny’ ‘stringr’

N  checking R code for possible problems (3.1s)
   mod_scatter_display_server : <anonymous>: no visible binding for global
     variable ‘movies’
   scatter_plot: no visible binding for global variable ‘.data’
   Undefined global functions or variables:
     .data movies

W  checking for missing documentation entries ...
   Undocumented code objects:
     ‘mod_scatter_display_server’ ‘mod_scatter_display_ui’
     ‘mod_var_input_server’ ‘mod_var_input_ui’ ‘scatter_plot’
   All user-level objects in a package should have documentation entries.
   See chapter ‘Writing R documentation files’ in the ‘Writing R
   Extensions’ manual.

0 errors ✔ | 2 warnings ✖ | 2 notes ✖

A summary of each item is below:

  • checking top-level files: This note refers to the two non-standard (i.e., not typically found in an R package) files, app.R and movies.RData.

  • checking dependencies in R code: This warning tells I need to namespace functions from add-on packages (in this case, ggplot2, shiny, and stringr)

  • checking R code for possible problems: This item refers to the call to load the movies data in the module server function (mod_scatter_display_server).

  • checking for missing documentation entries: This is warning me that the module functions aren’t properly documented and refers me to the official R documentation.

Each of these items is also printed under the ── R CMD check results heading:

show/hide R CMD check results
Duration: 15.3s

 checking dependencies in R code ... WARNING
  '::' or ':::' imports not declared from:
    ‘ggplot2’ ‘shiny’ ‘stringr’

 checking for missing documentation entries ... WARNING
  Undocumented code objects:
    ‘mod_scatter_display_server’ ‘mod_scatter_display_ui’
    ‘mod_var_input_server’ ‘mod_var_input_ui’ ‘scatter_plot’
  All user-level objects in a package should have documentation entries.
  See chapter ‘Writing R documentation files’ in the ‘Writing R
  Extensions’ manual.

 checking top-level files ... NOTE
  Non-standard files/directories found at top level:
    ‘app.R’ ‘movies.RData’

 checking R code for possible problems ... NOTE
  mod_scatter_display_server : <anonymous>: no visible binding for global
    variable ‘movies’
  scatter_plot: no visible binding for global variable ‘.data’
  Undefined global functions or variables:
    .data movies

0 errors ✔ | 2 warnings ✖ | 2 notes ✖

If you’re submitting your app-package to CRAN (or want to use check() for other reasons), follow the suggested workflow for check():

The workflow for checking a package is simple, but tedious:

  1. Run devtools::check() or press Shift + Ctrl/Cmd + E

  2. Fix the first problem.

  3. Repeat until there are no more problems.’

I’ve found a good habit for when to check() to be:

After adding a bug fix or feature, check a package and keep any notes, warnings, or errors from accumulating.

4.6 Hidden package files

You might notice additional ‘hidden’ files in your new app-package:8 .gitignore, .Rbuildignore, and .Rprofile:

4.6.1 .gitignore

.gitignore will ignore some of the standard hidden files created by R or RStudio. The initial contents will include something like the following:

.Rproj.user
.Rhistory
.RData
.Ruserdata
.DS_Store # for mac users 

4.6.2 .Rbuildignore

.Rbuildignore includes files that we need to have in our app-package, but don’t conform to the standard R package structure (and shouldn’t be included when building our app-package from the source files).

^.*\.Rproj$
^\.Rproj\.user$

Note the syntax for detecting file patterns.

4.6.3 .Rprofile

The .Rprofile is specific to the user (you) and might include options for loading packages or tests:

if (interactive()) {
  require(usethis, quietly = TRUE)
}
options(shiny.testmode = TRUE)

.Rprofile is also included in your directory if you’re using renv to manage packages/versions.

Recap

Creating an app-package involves adopting some new devtools habits, and the initial contents of sap hopefully helped demonstrate the purpose of each function.

Recap: Package development habits

After installing and loading devtools:

  1. Load the package whenever changes occur in the R/ folder.

    • Ctrl/Cmd + Shift + L load all the code in the package.
  2. Document the package whenever changes are made to any roxygen2 syntax (or settings).

    • Ctrl/Cmd + Shift + D record the documentation and dependencies.
  3. Install the package after the initial setup, after major changes to the code, documentation, or dependencies, and before committing or sharing.

    • Ctrl/Cmd + Shift + B confirms the package can be installed.

    • pak::local_install() benefits from optimized dependency resolution and download methods

    • devtools::install() handles a broader range of tasks during installation (including rebuilding documentation and running tests)

Habits require repetition to develop, and I hope the workflow above can be applied to your Shiny app-packages, provided you’re using devtools and Posit workbench.

The following section will cover documenting functions with roxygen2

Please open an issue on GitHub


  1. View the DESCRIPTION file in the 03.1_description branch here.↩︎

  2. If you create or convert your Shiny app project with usethis::create_package(), a few fields (i.e., Roxygen and RoxygenNote) are added automatically without explaining their role or purpose.↩︎

  3. If you created your Shiny app using the New Project Wizard, your .Rproj file has been configured to work with project, not a package.↩︎

  4. The topics covered in this section shouldn’t be considered a replacement for the ‘Whole Game’ chapter in R packages (2 ed) or the ‘Workflow’ section of Mastering Shiny (and I highly recommend reading both).↩︎

  5. devtools relies on roxygen2 for package documentation, so the RoxygenNote field is required in the DESCRIPTION.↩︎

  6. Always leave an empty final line in the DESCRIPTION file.↩︎

  7. It stands to reason that installing a package with pak::local_install() in Positron would be faster than installing a package using devtools::install() in RStudio, but this has not been my experience.↩︎

  8. By convention, files that begin with . (dot files) are considered hidden.↩︎