| Package | Version | Title | Description |
|---|---|---|---|
| leprechaun | 1.0.0 | Create Simple ‘Shiny’ Applications as Packages | Code generator for robust dependency-free ‘Shiny’ applications in the form of packages. It includes numerous convenience functions to create modules, include utility functions to create common ‘Bootstrap’ elements, setup a project from the ground-up, and much more. |
| packer | 0.1.3 | An Opinionated Framework for Using ‘JavaScript’ | Enforces good practice and provides convenience functions to make work with ‘JavaScript’ not just easier but also scalable. It is a robust wrapper to ‘NPM’, ‘yarn’, and ‘webpack’ that enables to compartmentalize ‘JavaScript’ code, leverage ‘NPM’ and ‘yarn’ packages, include ‘TypeScript’, ‘React’, or ‘Vue’ in web applications, and much more. |
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”
Package versions
The versions of leprechaun and packer used in this example are below:
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: usethisCreate 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 sessionAfter 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 filesThe 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,htmltoolsandpkgloadwill automatically be added to theDESCRIPTIONfile- The remaining
DESCRIPTIONfields need to be entered manually (or with thedescpackage). See example below:
- The remaining
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:
pkgloadLeave 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 in Positron ![]()
Development
The development workflow for a leprechaun application is similar to developing any app-package:
usethisfunctions can be used to add R files, tests, vignettes, README.md/NEWS.md files, etc.use
devtoolsto 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.RUI 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()andleprechaun_handler_list()are used for “converting the input received from the WebSocket to a data.frame/list.”.onAttach()registers the two input handlers aboveregisterInputHandler(): “When called, Shiny will use the function provided to refine the data passed back from the client (after being deserialized byjsonlite) before making it available in the input variable of theserver.Rfile”)
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_inputmodule we’ll run:leprechaun::add_module("vars")- This creates
R/module_vars.Rwith 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')- This creates
Note the send_message <- make_send_message(session) in var_input_server(). We’ll cover how this is used in the JavaScript section below.
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. Expand 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(
list(assets()),
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():
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.pngThen 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():
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 nodeInstall the R package:
install.packages('packer')
# or
pak::pak("JohnCoene/packer")Build the packer scaffolding:9
── 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 filesNow that the packer scaffolding is in place, we can run leprechaun::use_packer(), which creates the inst/dev/packer.R and adds packer to the DESCRIPTION.
leprechaun::use_packer()
✔ Creating inst/dev/packer.R
✔ Adding 'packer' to Suggests in DESCRIPTION
! This requires `leprechaun::build()` or the
! `leprechaun::build_roclet`The inst/dev/packer.R file contains the following:
show/hide inst/dev/packer.R
#' 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.
leprechaun::build()✔ Running packer.R
✔ Bundled leprechaun::build() runs the contents of inst/dev/packer.R to bundle the JavaScript code.
“Do not call this function from within the app. It helps build things, not run them.” -
build.mdguide
Lets review the new files that have been added to lap:
In the inst/dev/ folder, the packer.R file has been added, which calls packer::bundle():
inst/dev/
└── packer.R
1 directory, 1 fileIn the srcjs/ folder, index.js and modules/message.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.jsAdd the following to R/server.R
send_message <- make_send_message(session)
send_message("show-packer",
text = "a message from R/server.R")We can also include messages from modules.
R/module_plot_display.R
send_message <- make_send_message(session)
send_message("show-packer",
text = "a message from R/module_plot_display.R")After running devtools::load_all() and devtools::document(), the application loads with an alert.
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.10
leprechaun::use_sass()
✔ 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`New files in scss/:
scss
├── _core.scss
└── main.scss
1 directory, 2 filesThe scss/ folder created by leprechaun::use_sass() includes _core.scss and main.scss. We’ll change the original .error in _core.scss from red to green level-four-headers (h4) using $accent: #38B44A;:
/* original file */
html{
.error {
color: red
}
}/* updated file */
$accent: #38B44A;
html{
h4 {
color: $accent;
}
}Then run leprechaun::build() to bunlde the SCSS files.
leprechaun::build()
✔ Running packer.R
✔ Bundled
✔ Running sass.RThe 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(), and devtools::install(), then run():
The assets/ folder contains the files generated by the .R scripts in the dev/ folder.
inst/assets/
inst/assets/
├── index.js
└── style.min.css
1 directory, 2 filesinst/dev/
inst/dev
├── packer.R
└── sass.R
1 directory, 2 filesinst/dev/sass.R creates inst/assets/style.min.css and inst/dev/packer.R creates inst/assets/index.js.
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 "assets/style.min.css"
..- attr(*, "names")= chr "file"
$ head : NULL
$ attachment: NULL
$ package : chr "lap"
$ all_files : logi TRUE
- attr(*, "class")= chr "html_dependency"The output 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).
Writing code recap
leprechaun app-packages have a flexible and lightweight structure, and they are less opinionated than golem.
Adding modules
The add_module() function can be used to create new module files. The naming convention is less standardized than golems helper function, but the functionality is the same.
Adding utility functions
leprechaun comes with utilities (_utils) helpers (use_html_utils(), use_endpoints_utils(), use_js_utils()), and additional functions can be created using the usethis package.
Adding non-R files
Non-R files (JavaScript, SCSS, etc.) are included in the respective inst/assets/ folder, which an accompanying helper function in inst/dev.
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
Include the call to thematic::thematic_shiny() in the R/run.R file (above the call to shinyApp()).
run <- function(...){
thematic::thematic_shiny()
shinyApp(
ui = ui,
server = server,
...
)
}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()Unit tests
The unit tests for the utility function and the module server functions are in the tests/testthat/ folder:
tests
├── testthat
│ ├── helper.R
│ ├── test-module_aes.R
│ ├── test-module_scatter_display.R
│ ├── test-module_vars.R
│ └── test-utils_scatter_plot.R
└── testthat.R
2 directories, 6 filesleprechaun relies on the testthat framework for testing.11 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 in lap similar to those in golem:
library(shinytest2)
test_that("{shinytest2} recording: leprechaun app", {
app <- AppDriver$new(name = "lap-01", height = 800, width = 1173)
app$set_inputs(`vars-y` = "imdb_num_votes")
app$set_inputs(`vars-x` = "critics_score")
app$set_inputs(`vars-z` = "genre")
app$set_inputs(`aes-alpha` = 0.7)
app$set_inputs(`aes-size` = 3)
app$set_inputs(`aes-plot_title` = "New plot title")
app$expect_values()
})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().
leprechaun::use_config()
Adds inst/config.yml and R/config.R and includes yaml to the Imports field in our DESCRIPTION.
inst/config.yml
Defaults to production: true, which can be read using config_read() in R/config.R.
lap:::config_read()
$production
[1] TRUEValues can be added to inst/config.yml using the config file format, then the CONFIG_FILE can be set as an environmental variable
Dependencies
Is our leprechaun app ‘leaner and smaller’ than a golem app? Well, 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).
For example, lap depends on shiny, but not leprechaun.
pak::local_dev_deps_explain(
deps = "shiny",
root = "_apps/lap")
lap -> shinypak::local_dev_deps_explain(
deps = "leprechaun",
root = "_apps/lap")
x leprechaunHowever, 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 -> packerpak::local_dev_deps_explain(
deps = "sass",
root = "_apps/lap")
lap -> bslib -> sass
lap -> shiny -> bslib -> sass
lap -> packer -> htmlwidgets -> rmarkdown ->
bslib -> sass
lap -> sassThe 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).”
The final folder tree for lap (a leprechaun app-package) is below.
├── DESCRIPTION
├── NAMESPACE
├── R/
│ ├── _disable_autoload.R
│ ├── assets.R
│ ├── config.R
│ ├── data.R
│ ├── input-handlers.R
│ ├── lap_theme.R
│ ├── leprechaun-utils.R
│ ├── module_aes.R
│ ├── module_scatter_display.R
│ ├── module_vars.R
│ ├── run.R
│ ├── server.R
│ ├── ui.R
│ ├── utils-scatter_plot.R
│ ├── utils-tests.R
│ └── zzz.R
├── README.Rmd
├── 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
├── srcjs/
│ ├── config
│ ├── index.js
│ ├── leprechaun-utils.js
│ └── modules
├── webpack.common.js
├── webpack.dev.js
├── webpack.prod.js
├── scss/
│ ├── _core.scss
│ └── main.scss
└── tests/
├── testthat
└── testthat.R
109 directories, 63 files- 1
-
leprechaunapps are packages, so theinst/folders are available to the application at runtime. - 2
-
Files and folders created with
packer::scaffold_leprechaun()
- 3
-
Files and folders created with
leprechaun::use_sass()
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
├── config.yml
├── 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-logo.png
└── run
└── app.RAs we can see, none of the additional files and folders created with leprechaun are installed from the source code.
Recap
leprechaun delivers on its promise to be a ‘leaner and smaller’ version of golem.
- Tailored for developers who want to use modern JavaScript, HTML, and CSS workflows.
- Multiple
inst/sub-folders makes adding assets to the application easier, andleprechaunhas a long list ofuse_*functions for including Sass, CSS, HTML, and JavaScript.
- Multiple
leprechaundoesn’t offer much help with testing, although this can be done usingtestthatandshinytest2(just as we would with a standard R package), andleprechaunleaves many of the architectural decisions to the developer.- 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() |
|
The initial ‘setup’ function that builds the leprechaun files in R/ and inst/ |
scaffold_leprechaun() (from packer) |
|
This function comes from the This creates the following non-standard R package folders and files:
These non-standard folders are added to the |
use_packer() |
Assumes scaffold_leprechaun() from packer has been run |
Sets up application to use
|
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 Adds |
|
use_html_utils() |
Adds R/html-utils.R |
Adds
|
use_endpoints_utils() |
Adds R/endpoint-utils.R |
Adds
|
use_js_utils() |
Adds Adds import statement to |
Requires running leprechaun::build() to bundle .js |
use_config() |
Adds
|
Adds
|
add_module() |
Using
|
It would be nice if the modules had consistent naming (the UI function uses By default, modules aren’t exported (i.e., with Each module automatically includes:
|
add_app_file() |
Adds Includes call to |
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
The
leprechaun::scaffold()defaults to anavbarPage(), but you can switch to afluidPage()or change the Bootstrap version (“If shiny > 1.6 is installed defaults to version 5, otherwise version 4” ).↩︎We can remove
R/hello.Randman/hello.Rdfiles. These are just examples fromusethis::create_package().↩︎assets,dev, andimgwill initially contain.gitkeepfiles (a convention used by developers to force Git to include an otherwise empty directory in a repository).↩︎inst/run/app.Ris not run directly. Theleprechaun::add_app_file()will create anapp.Rfile for your app-package.↩︎Add a module with
leprechaun::add_module().↩︎Add a configuration file with
leprechaun::use_config(quiet = FALSE).↩︎Note the
R/server.Rfunction is not exported from the package by default (i.e., the@noRdtag is included with@keywords internal).↩︎packer::scaffold_leprechaun()initializes thenpmpackage manager for JavaScript, installswebpack, and adds the necessary JavaScript files and folders↩︎This Sass example is from the package website.↩︎
testthatcan be used for testing utility functions and module functions (as shown here).↩︎




