The leprechaun framework

leprechaun apps are built much like standard R packages (with devtools and usethis), but they’re designed with the intention of being a ‘leaner and smaller’ version of golem:1

“it generates code and does not make itself a dependency of the application you build; this means applications are leaner, and smaller”

Getting started

Create a leprechaun app just like you would a new R package:

usethis::create_package("lap")
 Creating '../projects/lap/'
 Setting active project to '/Users/mjfrigaard/projects/lap'
 Creating 'R/'
 Writing 'DESCRIPTION'
 Writing 'NAMESPACE'
 Writing 'lap.Rproj'
 Adding '^lap\\.Rproj$' to '.Rbuildignore'
 Adding '.Rproj.user' to '.gitignore'
 Adding '^\\.Rproj\\.user$' to '.Rbuildignore'
 Opening '/Users/mjfrigaard/projects/lap/' in new RStudio session

After the new project opens, install and load the leprechaun package, then run leprechaun::scaffold():2

install.packages("leprechaun")
library(leprechaun)
leprechaun::scaffold(ui = "fluidPage")

Package files

leprechaun::scaffold() results in the following folder tree:

├── DESCRIPTION
├── NAMESPACE
├── R/
   ├── _disable_autoload.R
   ├── assets.R
   ├── hello.R
   ├── input-handlers.R
   ├── leprechaun-utils.R
   ├── run.R
   ├── server.R
   ├── ui.R
   └── zzz.R
├── inst/
   ├── assets
   ├── dev
   ├── img
   └── run
       └── app.R
├── lap.Rproj
└── man
    └── hello.Rd

8 directories, 14 files

The standard R package files and folders (DESCRIPTION, NAMESPACE, R/, and lap.Rproj) are accompanied by multiple sub-folders in inst/ (recall that inst/ contents are available in the package when the package is installed).3

leprechaun files

The initial application files are created using leprechaun::scaffold(), which takes the following options as function arguments:

  • ui controls the application layout (can be "fluidPage" or "navbarPage", defaults to "navbarPage")
  • bs_version Bootstrap version (“If shiny > 1.6 is installed defaults to version 5, otherwise version 4” )
  • overwrite: Overwrite all files?

The output from leprechaun::scaffold() is below:

── Scaffolding leprechaun app ─────────────────────────────────────────

── Creating lock file ──

 Creating .leprechaun

The .leprechaun lock file contains the package name, version, as well as versions of bootstrap and accompanying R files:

{
  "package": "lap",
  "version": "1.0.0",
  "bs_version": 5,
  "r": {
    "ui": "1.0.0",
    "assets": "1.0.0",
    "run": "1.0.0",
    "server": "1.0.0",
    "leprechaun-utils": "1.0.0",
    "zzz": "1.0.0",
    "inputs": "1.0.0"
  }
}

leprechaun::scaffold() will automatically add shiny, bslib, htmltools and pkgload to the DESCRIPTION file:

── Adding dependencies ──

 Adding 'shiny' to Imports in DESCRIPTION
 Adding 'bslib' to Imports in DESCRIPTION
 Adding 'htmltools' to Imports in DESCRIPTION
 Adding 'pkgload' to Suggests in DESCRIPTION

The remaining DESCRIPTION fields need to be entered manually (or with the desc package). See example below:

Package: lap
Title: leprechaun app-package 
Version: 0.0.0.9000
Author: John Smith <John.Smith@email.io> [aut, cre]
Maintainer: John Smith <John.Smith@email.io>
Description: A movie-review leprechaun shiny application.
License: GPL-3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
LazyData: true
Imports: 
    bslib,
    htmltools,
    shiny
Suggests: 
    pkgload
1
Leave an empty line in the DESCRIPTION file

App code

The application code files are created in the R/ folder:

── Generating code ──

 Creating R/ui.R
 Creating R/assets.R
 Creating R/run.R
 Creating R/server.R
 Creating R/leprechaun-utils.R
 Creating R/_disable_autoload.R
 Creating R/zzz.R
 Creating R/input-handlers.R
  • ui.R holds the ui() and assets() functions. assets() loads the resources called in the R/assets.R file (see serveAssets() function above).

  • assets.R: contains the serveAssets() function, which will identify the modules using CSS or JavaScript and create dependencies, a list of metadata on the app

  • run.R contains functions for running the production (run()) and development version of the application (run_dev()):

  • server.R by default creates send_message with make_send_message(session) (see R/leprechaun-utils.R above).

  • leprechaun-utils.R initially contains the make_send_message() function (which is used in the R/server.R below)

  • _disable_autoload.R disables Shiny’s loadSupport(). By default, Shiny will load “any top-level supporting .R files in the R/ directory adjacent to the app.R/server.R/ui.R files.”

  • zzz.R contains .onLoad(), a wrapper for system.file() and Shiny’s addResourcePath() and function (used for adding images to the application in inst/img/).

  • input-handlers.R: contains leprechaun_handler_df() and leprechaun_handler_list() for “converting the input received from the WebSocket to a data.frame/list”

  • .onAttach() registers the two input handlers above

    • registerInputHandler(): “When called, Shiny will use the function provided to refine the data passed back from the client (after being deserialized by jsonlite) before making it available in the input variable of the server.R file”)

The initical call to devtools::document() will create documentation for the following files and includes RoxygenNote 7.2.3 in the DESCRIPTION:

 Updating lap documentation
First time using roxygen2. Upgrading automatically...
Setting `RoxygenNote` to "7.2.3"
 Loading lap
Warning: Skipping NAMESPACE
 It already exists and was not generated by roxygen2.
Writing serveAssets.Rd
Writing js-modules.Rd
Writing leprechaun_handler_df.Rd
Writing leprechaun_handler_list.Rd
Writing run.Rd
Writing run_dev.Rd
Writing ui.Rd
Writing assets.Rd
Warning: Skipping NAMESPACE 
 It already exists and was not generated by roxygen2. 
Documentation completed
1
The NAMESPACE file was initially with exportPattern("^[[:alpha:]]+"). To override this pattern and have the NAMESPACE file generated by roxygen2, delete the NAMESPACE and re-run devtools::document().

The inst/ folder

The inst/ folder contains the initial leprechaun scaffolding folders and inst/run/app.R file:

 Creating inst/dev
 Creating inst/assets
 Creating inst/img
 Creating inst/run/app.R
  • The inst/run/app.R contains calls to leprechaun::build() and pkgload::load_all() before running the app with run()

    show/hide inst/run/app.R
    # do not deploy from this file
    # see leprechaun::add_app_file()
    leprechaun::build()
    
    pkgload::load_all(
        path = "../../",
        reset = TRUE,
        helpers = FALSE
    )
    
    run()
    • inst/run/app.R is not run directly. The leprechaun::add_app_file() will create an app.R file for your app-package.
  • assets, dev, and img will initially contain .gitkeep files (a convention used by developers to force Git to include an otherwise empty directory in a repository). Each of these files will be demonstrated in the sections below.

The .leprechaun lock file is also added to the .Rbuildignore

── Ignoring files ──

 Adding '^\\.leprechaun$' to '.Rbuildignore'

Building leprechaun apps

Building leprechaun apps is similar to developing an R package. New code is placed in the R/ folder, and application resources (CSS, SASS, JavaScript files) are added using one of the leprechaun::use_* functions:

More assets can be added using the leprechaun::use_packer() function.

Modules

leprechaun has an add_module() helper function for creating modules:

  • To create the initial var_input module we’ll run:

    leprechaun::add_module("var_input")
    • This creates R/module_var_input.R with functions for the UI and server portions of the Shiny module:
    #' var_input UI
    #' 
    #' @param id Unique id for module instance.
    #' 
    #' @keywords internal
    var_inputUI <- function(id){
        ns <- NS(id)
    
        tagList(
            h2("var_input")
        )
    }
    
    #' var_input Server
    #' 
    #' @param id Unique id for module instance.
    #' 
    #' @keywords internal
    var_input_server <- function(id){
        moduleServer(
            id,
            function(
                input, 
                output, 
                session
                ){
    
                    ns <- session$ns
                    send_message <- make_send_message(session)
    
                    # your code here
            }
        )
    }
    
    # UI
    # var_inputUI('id')
    
    # server
    # var_input_server('id')

Note the send_message <- make_send_message(session) in var_input_server(). We’ll cover how this is used in the JavaScript section below.

Tip: @keywords internal

The module contents are similar to golem, but instead of using the @noRd tag, these functions include @keywords internal (which can be used to document your package).

The code for the var_input and plot_display modules are below.

  • The R/module_var_input.R file: plot_dispay collects the data from var_input and creates the plot with the custom scatter_plot() function:
var_inputUI <- function(id) {
  ns <- NS(id)
  tagList(
    selectInput(
      inputId = ns("y"),
      label = "Y-axis:",
      choices = c(
        "IMDB rating" = "imdb_rating",
        "IMDB number of votes" = "imdb_num_votes",
        "Critics Score" = "critics_score",
        "Audience Score" = "audience_score",
        "Runtime" = "runtime"
      ),
      selected = "audience_score"
    ),
    selectInput(
      inputId = ns("x"),
      label = "X-axis:",
      choices = c(
        "IMDB rating" = "imdb_rating",
        "IMDB number of votes" = "imdb_num_votes",
        "Critics Score" = "critics_score",
        "Audience Score" = "audience_score",
        "Runtime" = "runtime"
      ),
      selected = "imdb_rating"
    ),
    selectInput(
      inputId = ns("z"),
      label = "Color by:",
      choices = c(
        "Title Type" = "title_type",
        "Genre" = "genre",
        "MPAA Rating" = "mpaa_rating",
        "Critics Rating" = "critics_rating",
        "Audience Rating" = "audience_rating"
      ),
      selected = "mpaa_rating"
    ),
    sliderInput(
      inputId = ns("alpha"),
      label = "Alpha:",
      min = 0, max = 1, step = 0.1,
      value = 0.5
    ),
    sliderInput(
      inputId = ns("size"),
      label = "Size:",
      min = 0, max = 5,
      value = 2
    ),
    textInput(
      inputId = ns("plot_title"),
      label = "Plot title",
      placeholder = "Enter plot title"
    )
  )
}

var_input_server <- function(id) {
  moduleServer(id, function(input, output, session) {
      ns <- session$ns
      send_message <- make_send_message(session)

      # your code here
      return(
        reactive({
          list(
            "y" = input$y,
            "x" = input$x,
            "z" = input$z,
            "alpha" = input$alpha,
            "size" = input$size,
            "plot_title" = input$plot_title
          )
        })
      )
    }
  )
}
  • The R/module_plot_display.R file
plot_displayUI <- function(id) {
  ns <- NS(id)
  tagList(
    tags$br(),
    tags$blockquote(
      tags$em(
        tags$h6(
          "The code for this application comes from the ",
          tags$a("Building web applications with Shiny",
            href = "https://rstudio-education.github.io/shiny-course/"
          ),
          "tutorial"
        )
      )
    ),
    plotOutput(outputId = ns("scatterplot"))
  )
}
plot_display_server <- function(id, var_inputs) {
  moduleServer(id, function(input, output, session) {

      ns <- session$ns
      send_message <- make_send_message(session)

      inputs <- reactive({
        plot_title <- tools::toTitleCase(var_inputs()$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
        )
      })
      output$scatterplot <- renderPlot({
        plot <- scatter_plot(
          # data --------------------
          df = movies,
          x_var = inputs()$x,
          y_var = inputs()$y,
          col_var = inputs()$z,
          alpha_var = inputs()$alpha,
          size_var = inputs()$size
        )
        plot +
          ggplot2::labs(
            title = inputs()$plot_title,
            x = stringr::str_replace_all(
                  tools::toTitleCase(inputs()$x), "_", " "),
            y = stringr::str_replace_all(
                  tools::toTitleCase(inputs()$y), "_", " ")) +
          ggplot2::theme_minimal() +
          ggplot2::theme(legend.position = "bottom")
      })
    }
  )
}

The other components of lap were created using the standard usethis package development functions.

Utility functions

usethis::use_r() was used to create the R/plot_display-utils.R file that holds the scatter_plot() function:

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)

}

After creating the modules and utility function, adding these to the UI (R/ui.R) and server (R/server.R) is straightforward.

UI

The R/ui.R file will contain the fluidPage() layout functions and module UI server functions:

#' Shiny UI
#'
#' Core UI of package.
#'
#' @param req The request object.
#'
#'
#' @keywords internal
ui <- function(req) {
  fluidPage(
    theme = bslib::bs_theme(version = 5),
    assets(),
    h1("lap"),
    ## New code -->
        sidebarLayout(
          sidebarPanel(
            var_inputUI("vars")
          ),
          mainPanel(
          # new image
          tags$img(
            src = "img/leprechaun.jpg",
            height = "25%",
            width = "25%"),
          plot_displayUI("plot")
          )
        )
    ## New code <--
  )
}

Server

The R/server.R file contains the module server functions.

#' Server
#'
#' Core server function.
#'
#' @param input,output Input and output list objects
#' containing said registered inputs and outputs.
#' @param session Shiny session.
#'
#' @noRd
#' @keywords internal
server <- function(input, output, session){

    send_message <- make_send_message(session)

  ## New code -->
   selected_vars <- var_input_server("vars")

   plot_display_server("plot", var_inputs = selected_vars)

   ## New code <--

}

By default, the R/server.R file contains the make_send_message() function (which will be demonstrated below).4

Data

After calling usethis::use_data_raw('movies'), I can use system.file() to locate the movies.RData file with the following code in data-raw/movies.R:

## code to prepare `movies` dataset goes here
pth <- system.file('extdata/movies.RData', package = 'lap')
load(pth)
usethis::use_data(movies, overwrite = TRUE)

Launch

To launch the application in lap with an app.R file, leprechaun has a add_app_file() function. This creates a file similar to inst/run/app.R:

# Launch the ShinyApp 
# do not remove to keep push deploy button
# from RStudio
pkgload::load_all(
    reset = TRUE,
    helpers = FALSE
)

run()

Now I can run devtools::load_all(), devtools::document(), restart and load the package, then run()

Figure 1: run lap

Unit tests

The unit tests for the scatter_plot() utility function and the module server functions are in the tests/testthat/ folder:

tests
├── testthat
   ├── test-module_plot_display.R
   ├── test-module_var_input.R
   └── test-utils_scatter_plot.R
└── testthat.R

2 directories, 4 files

leprechaun relies on the testthat framework for testing.5 I’ve included the BDD functions (describe() and it()) to make each behavior and test clear.

System tests

System tests can also be included with shinytest2. The examples in lap are similar to those in the golem application.

tests/
├── testthat/
   ├── _snaps/
   │   ├── app-feature-01
   │   └── shinytest2
   ├── setup-shinytest2.R
   ├── test-app-feature-01.R
   └── test-shinytest2.R
└── testthat.R

5 directories, 13 files

Adding files and images

We’ll demonstrate how the inst/ folder works by adding an image to the application. Lets assume I want to add leprechaun.jpg to my UI:

  • Start by adding the file to inst/img/:

    inst/
      └── img/
           └── leprechaun.jpg <- new image file!
  • Then add the img/ path to the code to UI:

    tags$img(
      src = "img/leprechaun.jpg",
      height = "25%",
      width = "25%")

Once again, run devtools::load_all() and devtools::document(), restarting and loading the package, then run the application with run():

Figure 2: Adding images to inst/img/

Adding resources

leprechaun stores external code files the inst/ folder (similar to the golem framework), but uses a combination of use_* and build() functions to add functionality to you app.

Using packer

To demonstrate how packer and leprechaun work together, we’ll walk through the JavaScript example from the package website step-by-step:

First we have to build the scaffolding for packer by running packer::scaffold_leprechaun():

packer::scaffold_leprechaun()
  • packer::scaffold_leprechaun() initializes the npm package manager for JavaScript, installs webpack, and adds the necessary JavaScript files and folders:

    ── Scaffolding leprechaun ──────────────────────────────────────
     Initialiased npm
     webpack, webpack-cli, webpack-merge installed with scope "dev" 
     Added npm scripts
     Created srcjs directory
     Created srcjs/config directory
     Created webpack config files
    
    ── Adding files to .gitignore and .Rbuildignore ──
    
     Setting active project to '/projects/apps/sfw/_apps/lap'
     Adding '^srcjs$' to '.Rbuildignore'
     Adding '^node_modules$' to '.Rbuildignore'
     Adding '^package\\.json$' to '.Rbuildignore'
     Adding '^package-lock\\.json$' to '.Rbuildignore'
     Adding '^webpack\\.dev\\.js$' to '.Rbuildignore'
     Adding '^webpack\\.prod\\.js$' to '.Rbuildignore'
     Adding '^webpack\\.common\\.js$' to '.Rbuildignore'
     Adding 'node_modules' to '.gitignore'
    
    ── Scaffold built ──
    
     Run `bundle` to build the JavaScript files
     Run `leprechaun::use_packer()`
    • The following files and folders will be added to the lap root folder:
    show/hide packer::scaffold_leprechaun() files
    ├── node_modules/
       ├── @discoveryjs
       ├── @jridgewell
       ├── @types
       ├── @webassemblyjs
       ├── @webpack-cli
       ├── @xtuc
       ├── acorn
       ├── acorn-import-assertions
       ├── ajv
       ├── ajv-keywords
       ├── browserslist
       ├── buffer-from
       ├── caniuse-lite
       ├── chrome-trace-event
       ├── clone-deep
       ├── colorette
       ├── commander
       ├── cross-spawn
       ├── electron-to-chromium
       ├── enhanced-resolve
       ├── envinfo
       ├── es-module-lexer
       ├── escalade
       ├── eslint-scope
       ├── esrecurse
       ├── estraverse
       ├── events
       ├── fast-deep-equal
       ├── fast-json-stable-stringify
       ├── fastest-levenshtein
       ├── find-up
       ├── flat
       ├── function-bind
       ├── glob-to-regexp
       ├── graceful-fs
       ├── has-flag
       ├── hasown
       ├── import-local
       ├── interpret
       ├── is-core-module
       ├── is-plain-object
       ├── isexe
       ├── isobject
       ├── jest-worker
       ├── json-parse-even-better-errors
       ├── json-schema-traverse
       ├── kind-of
       ├── loader-runner
       ├── locate-path
       ├── merge-stream
       ├── mime-db
       ├── mime-types
       ├── neo-async
       ├── node-releases
       ├── p-limit
       ├── p-locate
       ├── p-try
       ├── path-exists
       ├── path-key
       ├── path-parse
       ├── picocolors
       ├── pkg-dir
       ├── punycode
       ├── randombytes
       ├── rechoir
       ├── resolve
       ├── resolve-cwd
       ├── resolve-from
       ├── safe-buffer
       ├── schema-utils
       ├── serialize-javascript
       ├── shallow-clone
       ├── shebang-command
       ├── shebang-regex
       ├── source-map
       ├── source-map-support
       ├── supports-color
       ├── supports-preserve-symlinks-flag
       ├── tapable
       ├── terser
       ├── terser-webpack-plugin
       ├── undici-types
       ├── update-browserslist-db
       ├── uri-js
       ├── watchpack
       ├── webpack
       ├── webpack-cli
       ├── webpack-merge
       ├── webpack-sources
       ├── which
       └── wildcard
    ├── package-lock.json
    ├── package.json
    ├── srcjs/
       ├── config
       ├── index.js
       └── modules
    ├── webpack.common.js
    ├── webpack.dev.js
    └── webpack.prod.js
    
    106 directories, 36 files

Now that the scaffolding is in place, run leprechaun::use_packer():

leprechaun::use_packer()
  • leprechaun::use_packer() creates the inst/dev/packer.R and adds packer to the DESCRIPTION. The inst/dev/packer.R file contains the following:

    #' Bundle for Prod
    #' 
    #' Bundles packer using packer.
    packer_bundle <- function(){
        has_packer <- requireNamespace("packer", quietly = TRUE)
    
        if (!has_packer) {
            warning(
                "Requires `packer` package: `install.packages('packer')`\n", 
                "Skipping.",
                call. = FALSE
            )
            return()
        }
    
        packer::bundle()
    }
    
    packer_bundle()

The final step is to build or ‘bundle’ the JavaScript files with leprechaun::build()

leprechaun::build()
  • leprechaun::build() runs the contents of inst/dev/packer.R to bundle the JavaScript code:

     Running packer.R
     Bundled       

Lets review the new files that have been added to the lap:

  • In the inst/dev/ folder, the packer.R file has been added, which calls packer::bundle()

    inst/dev/
          └── packer.R
    
    1 directory, 1 file
  • In the srcjs/ folder, the modules/message.js and index.js create the alert with Shiny.addCustomMessageHandler

    srcjs/
        ├── config
           ├── entry_points.json
           ├── externals.json
           ├── loaders.json
           ├── misc.json
           └── output_path.json
        ├── index.js
        └── modules
            └── message.js
    // srcjs/modules/message.js
    export const message = (msg) => {
      alert(msg);
    }
    // srcjs/index.js
    import { message } from './modules/message.js';
    import 'shiny';
    
    // In shiny server use:
    // session$sendCustomMessage('show-packer', 'hello packer!')
    Shiny.addCustomMessageHandler('show-packer', (msg) => {
      message(msg.text);
    })

To use the JS message scripts in srcjs/, I add the following to R/server.R:

    send_message <- make_send_message(session)
    send_message("show-packer",
                  text = "this message is from your R/server.R file")

After running devtools::load_all() and devtools::document(), the application loads with an alert:

(a) send_message() from R/server.R
Figure 3: send_message()

We can also include messages from modules.

  • In R/module_plot_display.R
send_message <- make_send_message(session)
send_message("show-packer",
  text = "this is a message from your plot_display module")

Read more about sending JavaScript messages here on the shiny website.

Using Sass

We can add Sass styling to our leprechaun app using the use_sass() helper function (this Sass example is from the package website).

  • Run leprechaun::use_sass():
leprechaun::use_sass()
  • This will add files to assets/ and dev/ and we see the following messages:
 Creating scss
 Creating inst/dev/sass.R
 Adding 'sass' to Suggests in DESCRIPTION
 Adding '^scss$' to '.Rbuildignore'
! This requires `leprechaun::build()` or the `leprechaun::build_roclet`
  • Below are the new files in scss/:
scss
├── _core.scss
└── main.scss

1 directory, 2 files

The scss/ folder created by leprechaun::use_sass() includes _core.scss and main.scss.

  • _core.scss: the original file is below
html{
    .error {
        color: red
    }
}
  • We’ll change the color: from red to green (#38B44A) using $accent: #38B44A;
$accent: #38B44A;

html{
    h1 {
        color: $accent;
    }
}
leprechaun::build()
 Running packer.R
 Bundled       
 Running sass.R
  • The inst/dev/sass.R file contains a sass_build() function

    • sass_build() looks in the scss/ folder for main.scss and creates the inst/assets/style.min.css file.
    #' Build CSS
    #'
    #' Build the sass
    sass_build <- function() {
      has_sass <- requireNamespace("sass", quietly = TRUE)
    
      if (!has_sass) {
        warning(
          "Requires `sass` package: `install.packages('sass')`\n",
          "Skipping.",
          call. = FALSE
        )
        return()
      }
    
      output <- sass::sass(
        sass::sass_file(
          "scss/main.scss"
        ),
        cache = NULL,
        options = sass::sass_options(
          output_style = "compressed"
        ),
        output = "inst/assets/style.min.css"
      )
      invisible(output)
    }
    
    sass_build()

Once again, I run devtools::load_all(), devtools::document(), install and restart, then load the package and run()

Figure 4: run lap with new Sass

leprechaun::build()

The assets/ folder contains the files generated by the .R scripts in the dev/ folder.

“Do not call this function from within the app. It helps build things, not run them.” - build.md guide

  • Contents of inst/assets/:

    inst/assets/
            ├── index.js
            └── style.min.css
    
    1 directory, 2 files
  • Contents of inst/dev/:

    inst/dev/
          ├── packer.R
          └── sass.R
    
    1 directory, 2 files
  • inst/dev/sass.R creates inst/assets/style.min.css and inst/dev/packer.R creates inst/assets/index.js

serveAssets()

After running leprechaun::use_sass() and leprechaun::build(), we’ll check the serveAssets() function:

lap:::serveAssets()
[[1]]
List of 10
 $ name      : chr "lap"
 $ version   : chr "0.0.0.9000"
 $ src       :List of 1
  ..$ file: chr "."
 $ meta      : NULL
 $ script    : Named chr "assets/index.js"
  ..- attr(*, "names")= chr "file"
 $ stylesheet: Named chr [1:2] "assets/style.min.css" "html/R.css"
  ..- attr(*, "names")= chr [1:2] "file" "file"
 $ head      : NULL
 $ attachment: NULL
 $ package   : chr "lap"
 $ all_files : logi TRUE
 - attr(*, "class")= chr "html_dependency"

This shows that stylesheet has been updated with "assets/style.min.css" and script has been updated with "assets/index.js" (these files are loaded into the application when it runs).

Configure

leprechaun app configuration files use the config package (similar to golem). leprechaun doesn’t assume I’ll be using a config.yml file, but we can easily add one with leprechaun::use_config().

  • use_config() adds a inst/config.yml and R/config.R

  • The default value in the config.yml files is production: true, which can be read using config_read() in R/config.R.

    config_read()
    $production
    [1] TRUE

Dependencies

Is my leprechaun app ‘leaner and smaller’ than my golem app?

leprechaun doesn’t add itself as a dependency (i.e., no need to add leprechaun to the list of Imports in the DESCRIPTION or NAMESPACE).

The section titled, ‘the golem in the room’ on the package website is worth reading because it covers the differences between the two packages (and why you might choose one over the other).

lap depends on shiny, but not leprechaun.

pak::local_dev_deps_explain(
  deps = "shiny", 
  root = "_apps/lap")

 lap -> shiny

 lap -> shinytest2 -> shiny
pak::local_dev_deps_explain(
  deps = "leprechaun", 
  root = "_apps/lap")

 x leprechaun

However, adding functionality and features with packer and the use_* functions can add dependencies to your leprechaun app:

pak::local_dev_deps_explain(
  deps = "packer", 
  root = "_apps/lap")

 lap -> packer
pak::local_dev_deps_explain(
  deps = "sass", 
  root = "_apps/lap")

 lap -> bslib -> sass

 lap -> shiny -> bslib -> sass

 lap -> packer -> htmlwidgets -> rmarkdown ->

   bslib -> sass

 lap -> sass

 lap -> shinytest2 -> rmarkdown -> bslib ->

   sass

 lap -> shinytest2 -> shiny -> bslib -> sass

The final folder tree for lap (a leprechaun app-package) is below.

├── DESCRIPTION
├── NAMESPACE
├── R/
   ├── _disable_autoload.R
   ├── assets.R
   ├── config.R
   ├── endpoint-utils.R
   ├── html-utils.R
   ├── input-handlers.R
   ├── leprechaun-utils.R
   ├── module_plot_display.R
   ├── module_var_input.R
   ├── run.R
   ├── server.R
   ├── ui.R
   ├── utils-js.R
   ├── utils_scatter_plot.R
   └── zzz.R
├── README.md
├── app.R
├── data/
   └── movies.rda
├── data-raw/
   └── movies.R
├── inst/
   ├── assets
   ├── config.yml
   ├── dev
   ├── extdata
   ├── img
   └── run
├── lap.Rproj
├── node_modules/
├── package-lock.json
├── package.json
├── scss/
   ├── _core.scss
   └── main.scss
├── srcjs/
   ├── config
   ├── index.js
   ├── leprechaun-utils.js
   └── modules
├── tests/
   ├── testthat
   └── testthat.R
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js

109 directories, 63 files
1
leprechaun apps are packages, so the inst/ folders are available to the application at runtime.
2
Files and folders created with packer::scaffold_leprechaun()

The output from system.file(".", package = "lap") has been passed to fs::dir_tree() below:

├── DESCRIPTION
├── INDEX
├── Meta/
   ├── Rd.rds
   ├── data.rds
   ├── features.rds
   ├── hsearch.rds
   ├── links.rds
   ├── nsInfo.rds
   └── package.rds
├── NAMESPACE
├── R/
   ├── lap
   ├── lap.rdb
   └── lap.rdx
├── assets/
   ├── index.js
   └── style.min.css
├── data/
   ├── Rdata.rdb
   ├── Rdata.rds
   └── Rdata.rdx
├── dev/
   ├── packer.R
   └── sass.R
├── extdata/
   └── movies.RData
├── help/
   ├── AnIndex
   ├── aliases.rds
   ├── lap.rdb
   ├── lap.rdx
   └── paths.rds
├── html/
   ├── 00Index.html
   └── R.css
├── img/
   └── leprechaun.jpg
└── run/
    └── app.R

As we can see, the files and folders created with packer::scaffold_leprechaun() won’t be installed with the source code.

Recap

leprechaun delivers on its promise to be a ‘leaner and smaller’ version of golem. Most of the features in golem are also accessible in leprechaun:

  • Adding modules: leprechaun’s add_module() function doesn’t have the consistent naming or prefixes found in golem::add_module(), but still reduces a lot of typing if you are creating these files manually.

  • Adding functions: leprechaun relies on usethis::use_r() for adding new functionality to your application

  • leprechaun doesn’t come with any testing functions, although this can be done using testthat and shinytest2 (just as we would with a standard R package).

Multiple inst/ sub-folders makes adding assets to the application easier, and leprechaun has a long list of use_* functions for including Sass, CSS, HTML, and JavaScript. The package website has examples for getting started and adding multiple resources, but unfortunately the function reference had limited documentation.

Below is an overview of the features/functions in the leprechaun framework:

Feature Arguments/Options Description/Comments
scaffold()

ui: can be one of "navbarPage" or "fluidPage"

bs_version: Bootstrap version

overwrite : recreate folder structure

The initial ‘setup’ function that builds the leprechaun files in R/ and inst/
scaffold_leprechaun() (from packer)

react : include React?

vue : include Vue?

use_cdn : use the CDN for react or vue dependencies?

edit : open pertinent files

This function comes from the packer package for integrating JS with R.

This creates the following non-standard R package folders and files:

  • srcjs/ and node_modules/
  • package-lock.json, package.json, webpack.common.js, webpack.dev.js, and webpack.prod.js files

These non-standard folders are added to the .Rbuildignore and .gitignore (but it would be nice to know more about what they do).

use_packer() Assumes scaffold_leprechaun() from packer has been run

Sets up application to use packer utilities for bundling JavaScript.

  • Creates inst/dev/packer.R
  • Adds packer to Suggests in DESCRIPTION
build() Returns TRUE/FALSE if build was successful. Used to ‘bundle’ various resources (i.e., from packer and the other use_ functions)
use_sass()

Creates inst/dev/sass.R

Adds sass to DESCRIPTION under Suggests

use_html_utils() Adds R/html-utils.R

Adds htmltools to Imports field in DESCRIPTION

R/html-utils.R contains a variety of utility functions using HTML tags (i.e., span(), div(), etc.)

use_endpoints_utils() Adds R/endpoint-utils.R

Adds jsonlite to Imports field in DESCRIPTION

R/endpoint-utils.R contains utility functions for creating an HTTP response object and the LEPRECHAUN_SERIALISER environmental variable.

use_js_utils()

Adds srcjs/leprechaun-utils.js and R/utils-js.R

Adds import statement to srcjs/index.js

Requires running leprechaun::build() to bundle .js
use_config()

Adds R/config.R and inst/config.yml

inst/config.yml initially contains production: true

Adds yml to Imports field in DESCRIPTION

R/config.R contains functions for reading and getting configuration values.

add_module()

Using add_module("name") :

  1. Creates R/module_name.R
  2. UI function: nameUI()
  3. Server function: name_server()

It would be nice if the modules had consistent naming (the UI function uses camelCase() and the server function is snake_case()).

By default, modules aren’t exported (i.e., with @export), but are included in the package index (i.e., with @keywords internal)

Each module automatically includes:

send_message <- make_send_message(session)

add_app_file()

Adds app.R file to root directory.

Includes call to pkgload::load_all() with reset set to TRUE and helpers set to FALSE

Handy for quickly launching the app during development (and deployment).
.onLoad() Used to add external files to app (images, html, etc.) Combines Shiny’s addResourcePath() and system.file().
serveAssets() modules argument can be used to include JavaScript modules Adds dependencies for JavaScript, CSS, and HTML.

Footnotes

  1. Download the code used to build the leprechaun app here↩︎

  2. The leprechaun::scaffold() defaults to a navbarPage(), but I’ve switched to a fluidPage() for this example.↩︎

  3. We can remove R/hello.R and man/hello.Rd files. These are just examples from usethis::create_package().↩︎

  4. Note the R/server.R function is not exported from the package by default (i.e., the @noRd tag is included with @keywords internal).↩︎

  5. testthat can be used for testing utility functions and module functions (as shown here).↩︎