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: usethis
Create a new package:
::create_package("lap") usethis
✔ 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")
::scaffold() leprechaun
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
andpkgload
will automatically be added to theDESCRIPTION
file- The remaining
DESCRIPTION
fields need to be entered manually (or with thedesc
package). 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: 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 in Positron
Development
The development workflow for a leprechaun
application is similar to developing any app-package:
usethis
functions can be used to add R files, tests, vignettes, README.md/NEWS.md files, etc.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
<- function(req){
ui navbarPage(
theme = bs_theme(version = 4),
header = list(assets()),
title = "lap",
id = "main-menu",
tabPanel(
"First tab",
::h1("First tab")
shiny
),tabPanel(
"Second tab",
::h1("Second tab")
shiny
)
) }
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
<- function(){
assets list(
serveAssets(), # base assets (assets.R)
$head(
tags# 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
<- function(input, output, session){
server <- make_send_message(session)
send_message }
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
<- function(modules = NULL) {
serveAssets # JavaScript files
<- list.files(
javascript system.file(package = "lap"),
recursive = TRUE,
pattern = ".js$"
)
<- get_modules(javascript, modules)
modules <- remove_modules(javascript, modules)
javascript
# CSS files
<- list.files(
css 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
<- list()
dependencies
<- htmlDependency(
standard "lap",
version = utils::packageVersion("lap"),
package = "lap",
src = ".",
script = javascript,
stylesheet = css
)<- append(dependencies, list(standard))
dependencies
if (!is.null(modules)) {
<- htmlDependency(
modules "lap-modules",
version = utils::packageVersion("lap"),
package = "lap",
src = ".",
script = modules,
meta = list(type = "module")
)<- append(dependencies, list(modules))
dependencies
}
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
<- function(files, modules) {
remove_modules if (is.null(modules)) {
return(files)
}
# make pattern
<- collapse_files(modules)
pattern
# remove modules
!grepl(pattern, files)]
files[ }
show/hide get_modules()
#' @rdname js-modules
#' @keywords internal
<- function(files, modules) {
get_modules if (is.null(modules)) {
return(NULL)
}
# make pattern
<- collapse_files(modules)
pattern
# remove modules
grepl(pattern, files)]
files[ }
show/hide collapse_files()
# collapse files into a pattern
<- function(files) {
collapse_files <- paste0(files, collapse = "$|")
pattern 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).
<- function(...){
run shinyApp(
ui = ui,
server = server,
...
) }
run_dev()
is for development purposes and launches a development version of our app (with local assets, etc.).
<- function(){
run_dev <- system.file(
file "run/app.R",
package = "lap"
)::shinyAppFile(file)
shiny }
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.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
<- function(session, prefix = NULL) {
make_send_message <- session$ns(NULL)
ns
<- ns
ns2 if (length(ns) > 0 && ns != "") {
<- paste0(ns2, "-")
ns2
}
function(msgId, ...) {
if (!is.null(prefix)) {
<- sprintf("%s-%s", prefix, msgId)
msgId
}
$sendCustomMessage(
session
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
<- function(data){
leprechaun_handler_df 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
<- function(data){
leprechaun_handler_list return(data)
}
show/hide .onAttach()
<- function(...) {
.onAttach ::registerInputHandler(
shiny"lap.list",
leprechaun_handler_list, force = TRUE
)
::registerInputHandler(
shiny"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:::add_module("vars") leprechaun
- 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 <- function(id){ varsUI <- NS(id) ns tagList( h2("var_input") ) } #' vars Server #' #' @param id Unique id for module instance. #' #' @keywords internal <- function(id){ vars_server moduleServer( id,function( input, output, session ){ <- session$ns ns <- make_send_message(session) send_message # 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
<- function(id){
varsUI <- NS(id)
ns 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
<- function(id){
vars_server moduleServer(
id,function(
input,
output,
session
){
<- session$ns
ns <- make_send_message(session)
send_message
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
<- function(id){
aesUI <- NS(id)
ns 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
<- function(id){
aes_server moduleServer(
id,function(
input,
output,
session
){
<- session$ns
ns <- make_send_message(session)
send_message
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
<- function(id){
scatter_displayUI <- NS(id)
ns tagList(
$br(),
tagsplotOutput(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
<- function(id, var_inputs, aes_inputs){
scatter_display_server moduleServer(
id,function(
input,
output,
session
){
<- session$ns
ns <- make_send_message(session)
send_message
<- reactive({
inputs <- tools::toTitleCase(aes_inputs()$plot_title)
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
)
})
$scatterplot <- renderPlot({
output<- scatter_plot(
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 ::labs(
ggplot2title = inputs()$plot_title,
x = stringr::str_replace_all(tools::toTitleCase(inputs()$x), "_", " "),
y = stringr::str_replace_all(tools::toTitleCase(inputs()$y), "_", " ")
+
) ::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
ggplot2
})
}
)
}
# 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
#'
<- function(df, x_var, y_var, col_var, alpha_var, size_var) {
scatter_plot ::ggplot(data = df,
ggplot2::aes(x = .data[[x_var]],
ggplot2y = .data[[y_var]],
color = .data[[col_var]])) +
::geom_point(alpha = alpha_var, size = size_var)
ggplot2 }
show/hide R/lap_theme.R
#' thematic leprechaun theme
#'
#' @returns bslib theme
#'
#' @export
#'
<- bslib::bs_theme(
lap_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
<- function(req){
ui tagList(
::page_fillable(
bsliblist(assets()),
title = "Movie Reviews (lap)",
theme = lap_theme,
::layout_sidebar(
bslibsidebar = bslib::sidebar(
varsUI("vars"),
aesUI("aes")
),::card(
bslibfull_screen = TRUE,
::card_body(
bslibscatter_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
<- function(input, output, session){
server <- make_send_message(session)
send_message
<- vars_server("vars")
selected_vars
<- aes_server("aes")
selected_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.png
Then add the img/
path to the code to UI:
::card_header(
bslib$h4(tags$em("Brought to you by ",
tags$img(
tagssrc = "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 node
Install the R package:
install.packages('packer')
# or
::pak("JohnCoene/packer") pak
Build the packer
scaffolding:9
── Scaffolding leprechaun ──────────────────────────
✔ Initialiased npm-cli, webpack-merge installed
✔ webpack, webpack"dev"
with scope
✔ Added npm scripts
✔ Created srcjs directory/config directory
✔ Created srcjs
✔ Created webpack config files
── Adding files to .gitignore and .Rbuildignore ──
"/path/to/lap".
✔ Setting active project to "^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.
✔ Adding
── Scaffold built ──
`bundle` to build the JavaScript files
ℹ Run `leprechaun::use_packer()` ℹ Run
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 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.
<- function(){
packer_bundle <- requireNamespace("packer", quietly = TRUE)
has_packer
if (!has_packer) {
warning(
"Requires `packer` package: `install.packages('packer')`\n",
"Skipping.",
call. = FALSE
)return()
}
::bundle()
packer
}
packer_bundle()
The final step is to build or ‘bundle’ the JavaScript files.
::build() leprechaun
✔ 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.md
guide
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 file
In 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.js
Add the following to R/server.R
<- make_send_message(session)
send_message send_message("show-packer",
text = "a message from R/server.R")
We can also include messages from modules.
R/module_plot_display.R
<- make_send_message(session)
send_message 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 files
The 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.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
<- function(){
sass_build <- requireNamespace("sass", quietly = TRUE)
has_sass
if(!has_sass){
warning(
"Requires `sass` package: `install.packages('sass')`\n",
"Skipping.",
call. = FALSE
)return()
}
<- sass::sass(
output ::sass_file(
sass'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 files
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
.
After running leprechaun::use_sass()
and leprechaun::build()
, we’ll check the serveAssets()
function:
:::serveAssets() lap
[[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 golem
s 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
<- system.file('extdata/movies.RData', package = 'lap')
pth load(pth)
::use_data(movies, overwrite = TRUE) usethis
Launch
Include the call to thematic::thematic_shiny()
in the R/run.R
file (above the call to shinyApp()
).
<- function(...){
run ::thematic_shiny()
thematicshinyApp(
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
::load_all(
pkgloadreset = 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 files
leprechaun
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", {
<- 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()
app })
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] TRUE
Values 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
.
::local_dev_deps_explain(
pakdeps = "shiny",
root = "_apps/lap")
-> shiny lap
::local_dev_deps_explain(
pakdeps = "leprechaun",
root = "_apps/lap")
x leprechaun
However, adding functionality and features with packer
and the use_*
functions can add dependencies to your leprechaun
app:
::local_dev_deps_explain(
pakdeps = "packer",
root = "_apps/lap")
-> packer lap
::local_dev_deps_explain(
pakdeps = "sass",
root = "_apps/lap")
-> bslib -> sass
lap
-> shiny -> bslib -> sass
lap
-> packer -> htmlwidgets -> rmarkdown ->
lap
-> sass
bslib
-> sass lap
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).”
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
-
leprechaun
apps 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.R
As 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, andleprechaun
has a long list ofuse_*
functions for including Sass, CSS, HTML, and JavaScript.
- Multiple
leprechaun
doesn’t offer much help with testing, although this can be done usingtestthat
andshinytest2
(just as we would with a standard R package), andleprechaun
leaves 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.R
andman/hello.Rd
files. These are just examples fromusethis::create_package()
.↩︎assets
,dev
, andimg
will initially contain.gitkeep
files (a convention used by developers to force Git to include an otherwise empty directory in a repository).↩︎inst/run/app.R
is not run directly. Theleprechaun::add_app_file()
will create anapp.R
file 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.R
function is not exported from the package by default (i.e., the@noRd
tag is included with@keywords internal
).↩︎packer::scaffold_leprechaun()
initializes thenpm
package manager for JavaScript, installswebpack
, and adds the necessary JavaScript files and folders↩︎This Sass example is from the package website.↩︎
testthat
can be used for testing utility functions and module functions (as shown here).↩︎