The leprechaun framework

leprechaun apps are built much like standard R packages (built 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”

IMPORTANT: Development Environment

This page covers developing a new leprechaun application using Positron, the new IDE from Posit. The current version information is below:

Positron Version: 2025.03.0 (Universal) build 116
Code - OSS Version: 1.96.0
Commit: 7f0f93873e8b24c9f393bb3586724f4783c5e72c
Date: 2025-03-01T18:51:44.014Z
Electron: 32.2.6
Chromium: 128.0.6613.186
Node.js: 20.18.1
V8: 12.8.374.38-electron.0
OS: Darwin x64 24.3.0

Getting started

Create a leprechaun app just like you would a new R package (install devtools, which includes usethis as a dependency):

install.packages('devtools')
library(devtools)
Loading required package: usethis

Create a new 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")
leprechaun::scaffold()

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/, etc.) are accompanied by multiple sub-folders in inst/ (recall that inst/ contents are available in the package when the package is installed).3

Getting Started

The following files are part of our initial application created with leprechaun::scaffold().

.leprechaun

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

DESCRIPTION

  • shiny, bslib, htmltools and pkgload will automatically be added to the DESCRIPTION file

    • 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

Leave an empty final line in the DESCRIPTION file.

As promised, leprechaun app-packages can be spun up quickly with minimal commands. After loading and documenting lap, we can use the standalone app function run() to launch the app.

Initial leprechaun app

Initial leprechaun app

Development

The development workflow for a leprechaun application is similar to developing any app-package:

  1. usethis functions can be used to add R files, tests, vignettes, README.md/NEWS.md files, etc.

  2. use devtools to load, document, and install.

Application code

Before we create any new files, we’re going to dive into the code that’s included in a new leprechaun app-package. Most of these files are in the R/ folder, but we can also new subfolders in inst/.

└── R/
    ├── _disable_autoload.R
    ├── assets.R
    ├── input-handlers.R
    ├── leprechaun-utils.R
    ├── run.R
    ├── server.R
    ├── ui.R
    └── zzz.R
└── inst/
    ├── assets
    ├── dev
    ├── img
    └── run
        └── app.R

UI and Server

ui.R holds the ui() and assets() functions, and server.R includes the core application server() function.

App UI

#' Shiny UI
#' 
#' Core UI of package.
#' 
#' @param req The request object.
#' 
#' @import shiny
#' @importFrom bslib bs_theme
#' 
#' @keywords internal
ui <- function(req){
    navbarPage(
        theme = bs_theme(version = 4),
        header = list(assets()),
        title = "lap",
        id = "main-menu",
        tabPanel(
            "First tab",
            shiny::h1("First tab")
        ),
        tabPanel(
            "Second tab",
            shiny::h1("Second tab")
        )
    )
}

assets() loads the resources called in the R/assets.R file.

#' Assets
#' 
#' Includes all assets.
#' This is a convenience function that wraps
#' [serveAssets] and allows easily adding additional
#' remote dependencies (e.g.: CDN) should there be any.
#' 
#' @importFrom shiny tags
#' 
#' @keywords internal
assets <- function(){
    list(
        serveAssets(), # base assets (assets.R)
        tags$head(
            # Place any additional depdendencies here
            # e.g.: CDN
        )   
    )
}

server() also includes make_send_message(session) from R/leprechaun-utils.R.

#' 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)  
}

Assets

The R/assets.R functions handle how JavaScript and CSS files are loaded and served to the browser.

serveAssets()

Scans and sorts JavaScript/CSS files and bundles everything as proper HTML dependencies that Shiny can use.

remove_modules()

Filters out the module files from the list of all JavaScript files.

get_modules()

Extracts only the module files from the list of all JavaScript files.

collapse_files()

Creates a regular expression pattern to match specific files.

Expand the code chunks below to view the functions in R/assets.R.

show/hide serveAssets()
#' Dependencies
#'
#' @param modules JavaScript files names that require
#' the `type = module`.
#' @importFrom htmltools htmlDependency
#'
#' @keywords internal
serveAssets <- function(modules = NULL) {
  # JavaScript files
  javascript <- list.files(
    system.file(package = "lap"),
    recursive = TRUE,
    pattern = ".js$"
  )

  modules <- get_modules(javascript, modules)
  javascript <- remove_modules(javascript, modules)

  # CSS files
  css <- list.files(
    system.file(package = "lap"),
    recursive = TRUE,
    pattern = ".css$"
  )

  # so dependency processes correctly
  names(css) <- rep("file", length(css))
  names(javascript) <- rep("file", length(javascript))

  # serve dependencies
  dependencies <- list()

  standard <- htmlDependency(
    "lap",
    version = utils::packageVersion("lap"),
    package = "lap",
    src = ".",
    script = javascript,
    stylesheet = css
  )
  dependencies <- append(dependencies, list(standard))

  if (!is.null(modules)) {
    modules <- htmlDependency(
      "lap-modules",
      version = utils::packageVersion("lap"),
      package = "lap",
      src = ".",
      script = modules,
      meta = list(type = "module")
    )
    dependencies <- append(dependencies, list(modules))
  }

  return(dependencies)
}
show/hide remove_modules()
#' Module
#'
#' Retrieve and add modules from a vector of files.
#'
#' @param files JavaScript files
#' @param modules JavaScript files names that require
#' the `type = module`.
#' @importFrom htmltools htmlDependency
#'
#' @keywords internal
#' @name js-modules
remove_modules <- function(files, modules) {
  if (is.null(modules)) {
    return(files)
  }

  # make pattern
  pattern <- collapse_files(modules)

  # remove modules
  files[!grepl(pattern, files)]
}
show/hide get_modules()
#' @rdname js-modules
#' @keywords internal
get_modules <- function(files, modules) {
  if (is.null(modules)) {
    return(NULL)
  }

  # make pattern
  pattern <- collapse_files(modules)

  # remove modules
  files[grepl(pattern, files)]
}
show/hide collapse_files()
# collapse files into a pattern
collapse_files <- function(files) {
  pattern <- paste0(files, collapse = "$|")
  paste0(pattern, "$")
}

Standalone app function

R/run.R contains functions to launch our Shiny application in different modes.

run() is the main function that users will call to launch our application in normal mode (production-ready).

run <- function(...){
    shinyApp(
        ui = ui,
        server = server,
        ...
    )
}

run_dev() is for development purposes and launches a development version of our app (with local assets, etc.).

run_dev <- function(){
    file <- system.file(
              "run/app.R", 
              package = "lap"
              )
    shiny::shinyAppFile(file)
}

External resources

The inst/ folder contains the initial leprechaun scaffolding folders (assets, dev, img, and run) and a single inst/run/app.R file.4

inst/assets

This folder is for storing front-end resources like JavaScript and CSS files (are automatically discovered by the serveAssets() function).

inst/dev

This folder contains development-related files and tools not needed in production.

inst/img

This folder contains static image files for the application. To add images to the application, R/zzz.R contains .onLoad(), a wrapper for system.file() and Shiny’s addResourcePath().

inst/run

The inst/run/app.R contains calls to leprechaun::build() and pkgload::load_all() before running the app with run().5

Utilities

R/leprechaun-utils.R

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

R/input-handlers.R

  • leprechaun_handler_df() and leprechaun_handler_list() are used 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”)

R/_disable_autoload.R

_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.”

Expand the code chunks below to view the functions in R/leprechaun-utils.R and R/input-handlers.R.

show/hide make_send_message()
#' Create a Helper to Send Messages
#'
#' Create a function to send custom messages to the front-end,
#' this function makes it such that the namespace is carried
#' along.
#' The namespace is appended as `ns`.
#' The namespace with the optional hyphen is
#' included as `ns2`.
#'
#' @param session Shiny session to derive namespace
#' @param prefix A prefix to add to all types.
#' Note that the prefix is followed by a hyphen `-`.
#'
#' @examples
#' \dontrun{
#' send_message <- make_send_message(session)
#' send_message("do-sth")
#' send_message("do-sth-else", x = 1)
#'
#' # with prefix
#' send_message <- make_send_message(session, prefix = "PREFIX")
#'
#' # this sends a mesasge of type:
#' # PREFIX-so-th
#' send_message("do-sth")
#' }
#'
#' @noRd
#' @keywords internal
make_send_message <- function(session, prefix = NULL) {
  ns <- session$ns(NULL)

  ns2 <- ns
  if (length(ns) > 0 && ns != "") {
    ns2 <- paste0(ns2, "-")
  }

  function(msgId, ...) {
    if (!is.null(prefix)) {
      msgId <- sprintf("%s-%s", prefix, msgId)
    }

    session$sendCustomMessage(
      msgId,
      list(
        ns = ns,
        ns2 = ns2,
        ...
      )
    )
  }
}
show/hide leprechaun_handler_df()
#' Input Dataframe
#' 
#' Converts the input received from the WebSocket
#' to a data.frame.
#' 
#' @param data Input data received from WebSocket.
#' 
#' @keywords internal
leprechaun_handler_df <- function(data){
    do.call("rbind", lapply(data))
}
show/hide leprechaun_handler_list()
#' Input List
#' 
#' Forces the input received from the WebSocket 
#' to a list. This should really not be needed as
#' it is handled like so by default.
#' 
#' @param data Input data received from WebSocket.
#' 
#' @keywords internal
leprechaun_handler_list <- function(data){
    return(data)
}
show/hide .onAttach()
.onAttach <- function(...) {
    shiny::registerInputHandler(
        "lap.list", 
        leprechaun_handler_list, 
        force = TRUE
    )

    shiny::registerInputHandler(
        "lap.df", 
        leprechaun_handler_df, 
        force = TRUE
    )
}

Application code recap

UI & Server

The UI and server functions in R/ui.R and R/server.R are the entry points of the leprechaun app, serving as wrappers that connect the app’s modules and centralize the UI layout and server logic (similar to golem apps).

Utility functions

leprechaun-utils.R contains utilities that help with communication between your R server and JavaScript front-end, and R/input-handlers.R sets up custom data handling between your JavaScript front-end and R back-end.

Assets

The functions in R/assets.R are called with the assets() function in R/ui.R, which calls serveAssets() and includes all the front-end resources in our app. The inst/assets/ folder will contain any custom JavaScript or CSS files (or SCSS/SASS files) for styling.

External resources

The inst/img/ folder will store images, which are loaded in the .onLoad() function in R/zzz.R file.

Writing code

Building leprechaun apps is similar to developing an R package. leprechaun has helper functions for adding modules6 and configuration7 files, but with fewer bells and whistles than golem.

Modules

leprechaun has an add_module() helper function for creating modules. We’ll use it to add the two inputs and scatter display modules.

module_scatter_display collects the data from module_vars and module_aes to create the plot with the custom scatter_plot() function:

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

    leprechaun::add_module("vars")
    • This creates R/module_vars.R with functions for the UI and server portions of the Shiny module:
    #' vars UI
    #' 
    #' @param id Unique id for module instance.
    #' 
    #' @keywords internal
    varsUI <- function(id){
        ns <- NS(id)
    
        tagList(
            h2("var_input")
        )
    }
    
    #' vars Server
    #' 
    #' @param id Unique id for module instance.
    #' 
    #' @keywords internal
    vars_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: Using @keywords internal

{pkgname}-package.R

The leprechaun module contents are similar to the modules created with the golem helper function, but instead of using the @noRd tag, leprechaun modules include @keywords internal, which can be used in combination with a {pkgname}-package.R file to document your package.

  1. Call usethis::use_package_doc()
  2. Add the following to {pkgname}-package.R:
#' @keywords internal 
"_PACKAGE"

The code for the vars, aes and scatter_display modules are below.

show/hide module_vars.R
#' vars UI
#'
#' @param id Unique id for module instance.
#'
#' @keywords internal
varsUI <- 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"
        )
      )
}

#' vars Server
#'
#' @param id Unique id for module instance.
#'
#' @keywords internal
vars_server <- function(id){
    moduleServer(
        id,
        function(
            input,
            output,
            session
            ){

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

      return(
        reactive({
          list(
            "y" = input$y,
            "x" = input$x,
            "z" = input$z
          )
        })
      )
        }
    )
}

# UI
# varsUI('id')

# server
# vars_server('id')
show/hide module_aes.R
#' aes UI
#'
#' @param id Unique id for module instance.
#'
#' @keywords internal
aesUI <- function(id){
    ns <- NS(id)
    tagList(
    sliderInput(
      inputId = ns("alpha"),
      label = "Alpha:",
      min = 0,
      max = 1,
      step = 0.1,
      value = 0.7
    ),
    sliderInput(
      inputId = ns("size"),
      label = "Size:",
      min = 0,
      max = 5,
      step = 0.5,
      value = 3
    ),
    textInput(
      inputId = ns("plot_title"),
      label = "Plot title",
      placeholder = "Enter plot title"
    )
    )
}

#' aes Server
#'
#' @param id Unique id for module instance.
#'
#' @keywords internal
aes_server <- function(id){
    moduleServer(
        id,
        function(
            input,
            output,
            session
            ){

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

      return(
        reactive({
          list(
            "alpha" = input$alpha,
            "size" = input$size,
            "plot_title" = input$plot_title
          )
        })
      )
        }
    )
}

# UI
# aesUI('id')

# server
# aes_server('id')
show/hide module_scatter_display.R
#' scatter_display UI
#'
#' @param id Unique id for module instance.
#'
#' @keywords internal
scatter_displayUI <- function(id){
    ns <- NS(id)
  tagList(
    tags$br(),
    plotOutput(outputId = ns("scatterplot"))
  )
}

#' scatter_display Server
#'
#' @param id Unique id for module instance.
#' @param var_inputs variable inputs
#' @param aes_inputs aesthetic inputs
#'
#' @keywords internal
scatter_display_server <- function(id, var_inputs, aes_inputs){
    moduleServer(
        id,
        function(
            input,
            output,
            session
            ){

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

    inputs <- reactive({
      plot_title <- tools::toTitleCase(aes_inputs()$plot_title)
        list(
          x = var_inputs()$x,
          y = var_inputs()$y,
          z = var_inputs()$z,
          alpha = aes_inputs()$alpha,
          size = aes_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")
    })
        }
    )
}

# UI
# scatter_displayUI('id')

# server
# scatter_display_server('id')

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

Utility functions

Use usethis::use_r() or create a new file in the R/ folder to add utility functions to lap. Expland the code chunks below to view our plotting utility function and bslib theme:

show/hide R/utils-scatter_plot.R
#' The scatter plot utility function
#'
#' @description A custom graphing ggplot2 function
#'
#' @return The return value, if any, from executing the utility.
#'
#' @param df `data.frame` or `tibble`
#' @param x_var string variable mapped to `x` axis
#' @param y_var string variable mapped to `y` axis
#'
#'
#' @importFrom rlang .data
#'
#' @export
#'
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)
}
show/hide R/lap_theme.R
#' thematic leprechaun theme
#'
#' @returns bslib theme
#'
#' @export
#'
lap_theme <- bslib::bs_theme(
  bg = "#ffffff",
  fg = "#1a1a1a",
  primary = "#3C9D5D",   # green primary
  secondary = "#CCCCCC",
  success = "#3C9D5D",
  info = "#17A2B8",
  warning = "#F0AD4E",
  danger = "#D9534F",
  accent = "#6C757D",
  base_font = bslib::font_google("Ubuntu"),
  heading_font = bslib::font_google("Raleway")
)

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 a navbarPage() layout by default, but we’ll update this with bslib.

show/hide R/ui.R
#' Shiny UI
#'
#' Core UI of package.
#'
#' @param req The request object.
#'
#' @import shiny
#' @importFrom bslib bs_theme
#'
#' @keywords internal
ui <- function(req){
  tagList(
      bslib::page_fillable(
        title = "Movie Reviews (lap)",
        theme = lap_theme,
        bslib::layout_sidebar(
          sidebar = bslib::sidebar(
            varsUI("vars"),
            aesUI("aes")
          ),
          bslib::card(
            full_screen = TRUE,
            bslib::card_body(
              scatter_displayUI("plot")
            )
          )
        )
      )
  )
}

Server

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

show/hide R/server.R
#' 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)

      selected_vars <- vars_server("vars")

      selected_aes <- aes_server("aes")

      scatter_display_server("plot",
                             var_inputs = selected_vars,
                             aes_inputs = selected_aes)
}

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

After loading, documenting, and installing lap, we can launch the application with run():

lap with modules and utility functions

lap with modules and utility functions

Adding images

To add an image to the UI, we save the file (leprechaun-logo.png) in the inst/ folder:

Save file to inst/img/

inst/
  └── img/
       └── leprechaun-logo.png

Then add the img/ path to the code to UI:

  bslib::card_header(
    tags$h4(tags$em("Brought to you by ",
        tags$img(
          src = "img/leprechaun-logo.png",
          height = 100,
          width = 100,
          style = "margin:10px 10px"
          )
        )
      )
    )

Run devtools::load_all(), devtools::document(), and devtools::install(upgrade = FALSE), then launch the application with run():

Adding images to inst/img/

Adding images to inst/img/

Assets (JavaScript)

leprechaun combines use_* and build functions to add functionality to Shiny apps. The external code files are stored in the inst/ folder.

We’ll briefly cover what this looks like with the packer JavaScript example from the package website. Be sure to install node and packer.

Enter the following in the Terminal:

brew update
brew install node

Install the R package:

install.packages('packer')
# or 
pak::pak("JohnCoene/packer")

packer::scaffold_leprechaun() builds the scaffolding for packer:9

packer::scaffold_leprechaun()
  ── 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 "/path/to/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 node_modules/ 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()

Issue: Issue with packer::scaffold_leprechaun() (or maybe 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:

send_message() from R/server.R

send_message() from R/server.R

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.

Assets (Sass)

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

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

Unit tests

System tests

Configure

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 you can switch to a fluidPage() or change the Bootstrap version (“If shiny > 1.6 is installed defaults to version 5, otherwise version 4” ).↩︎

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

  4. 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).↩︎

  5. inst/run/app.R is not run directly. The leprechaun::add_app_file() will create an app.R file for your app-package.↩︎

  6. Add a module with leprechaun::add_module().↩︎

  7. Add a configuration file with leprechaun::use_config(quiet = FALSE).↩︎

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

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