%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'monospace', "darkMode":false}}}%%
flowchart TD
Pkg(["<code>sap</code><br>app-package"])
subgraph Srv["<strong>Server</strong>"]
RS("R process on a host<br>(Connect Cloud, Docker)")
end
subgraph Web["<strong>Browser</strong>"]
Live[["<code>shinylive::export()</code>"]]
WR("<code>webR</code> session<br>in the browser tab")
end
subgraph Desk["<strong>Desktop</strong>"]
SE[["<code>shinyelectron::export()</code>"]]
EL("Electron window with<br>portable R or <code>webR</code>")
end
Pkg -->|"i. deploy"| RS
Pkg -->|"ii. export"| Live
Live --> WR
Pkg -->|"iii. export"| SE
SE --> EL
style Srv fill:#FFFFFF,color:#000000,stroke:#333,stroke-width:1px,rx:10,ry:10
style Web fill:#FFFFFF,color:#000000,stroke:#333,stroke-width:1px,rx:10,ry:10
style Desk fill:#FFFFFF,color:#000000,stroke:#333,stroke-width:1px,rx:10,ry:10
style Pkg fill:#FFFFFF,color:#000000,stroke:#333
style RS fill:#4CBB9D,color:#FFFFFF,rx:5,ry:5
style Live fill:#4CBB9D,color:#FFFFFF
style WR fill:#4CBB9D,color:#FFFFFF,rx:5,ry:5
style SE fill:#4CBB9D,color:#FFFFFF
style EL fill:#4CBB9D,color:#FFFFFF,rx:5,ry:5
I’ve finally added webR and shinyelectron chapters to my Shiny App-Packages text (under the Deploy section).
- webR chapter runs the app in the user’s browser
- shinyelectron chapter packages it as a desktop installer
In this post I’ll cover what each chapter does with sap (the book’s app-package), and a few of the problems I encountered along the way.
Where does the app run?
Every deployment in the earlier book chapters (Connect Cloud, Docker, GitHub Actions) needs a server. These two chapters move R (and the app) onto the user’s computer.
The diagram below shows where R runs in each case:
The chapters build on each other: The shinyelectron desktop chapter reuses the launcher from the webR browser chapter, and the Linux build depends on the WebAssembly binary we set up for the browser.
webR: the app in a browser tab
webR is R compiled to WebAssembly, a binary format that modern browsers run at near native speed. shinylive bundles a Shiny app, webR, and the app’s packages into a folder of static files. A static file host is just a web server that hands out files without running any code. GitHub Pages or Netlify can serve the app.
After the first download, every click stays in the browser.
What the browser doesnt’t do
The browser takes on the server’s job, but it can’t do all of it. In my opinion, these trade-offs matter most:
- The first time the app loads, the browser downloads
webRand every package before the app appears. - All the code and data go to the browser, so never include credentials.
- You can’t call on system tools or downloads (i.e.,
system()calls fail,sass::font_google()needslocal = FALSE)
- Temporary files like logs will disappear when the tab closes
Every package needs a binary
webR can’t install packages from source, so each package in Imports needs a WebAssembly binary. The chapter checks DESCRIPTION against the webR repository:
wasm_pkgs <- available.packages(
contriburl = "https://repo.r-wasm.org/bin/emscripten/contrib/4.6"
)
base_pkgs <- rownames(installed.packages(priority = "base"))
imports <- read.dcf("DESCRIPTION", fields = "Imports")
imports <- trimws(strsplit(imports, ",")[[1]])
imports <- imports[imports != ""]
setdiff(imports, c(rownames(wasm_pkgs), base_pkgs))An empty result (character(0)) means every dependency has a binary.
sap itself isn’t on CRAN, so it has no binary. R-universe builds one on each push. Setting it up takes a <username>.r-universe.dev repository, a packages.json file, and the R-universe GitHub app.
We then install sap from the universe, because shinylive uses each installed package’s source to find its binary.
A launcher, not launch_app()
shinylive exports a folder with an app.R file, not a package. The chapter adds a small launcher in inst/webr/app.R:
library(shiny)
library(sap)
shinyApp(
ui = movies_ui(bslib = TRUE),
server = movies_server
)The launcher returns a Shiny app object instead of calling the standalone app function (launch_app()). We use shinyApp() in inst/webr/app.R because shiny::runApp() starts a server and blocks the R session. The library(sap) call above is also important because shinylive scans app.R for library() calls to decide which binaries to bundle.
Exporting and previewing take two lines:
shinylive::export(appdir = "inst/webr", destdir = "_site")
httpuv::runStaticServer("_site")Opening _site/index.html directly won’t work, because webR needs its files served over HTTP.
shinyelectron: the app as a desktop program
Some users need the app as a program they install. They might work offline, or their organization might block unknown websites. Electron is a framework for desktop programs built with web technologies; an Electron app is just a web browser with a single page. shinyelectron wraps a Shiny app in Electron, so the user never sees a browser or a URL.
Choosing a runtime strategy
The app still needs R, and shinyelectron offers five ways to provide it. Each strategy trades installer size against what the user’s computer needs:
| Strategy | The installer ships | The user needs |
|---|---|---|
shinylive |
Electron and a webR bundle |
nothing |
bundled |
Electron and a portable copy of R | nothing |
auto-download |
Electron and a downloader | internet on first launch |
system |
Electron only | R installed |
container |
Electron and a container setup | Docker or Podman |
The chapter uses bundled for macOS and Windows, which installs sap from local source. Portable R only exists for those two platforms, and bundled can’t cross-build.
The electron/ folder
The package itself doesn’t change. All the desktop code lives in an electron/ folder: the same launcher app.R from the webR chapter, a _shinyelectron.yml configuration file, and a build.R script. The configuration file holds the setting I found most useful:
dependencies:
r:
local_packages:
- "../.."local_packages installs sap from the package root, which is how a bundled app gets a package that isn’t on CRAN. The CRAN release of shinyelectron (0.2.1) doesn’t support this field yet, so the chapter installs the development version from GitHub.
Building the .dmg
The build script checks the system with sitrep_shinyelectron(), validates the app with app_check(), and calls export(). On an Intel Mac, the result is a .dmg of about 256 MB, most of it portable R.
Most of the build warnings are harmless. The NB notes are worth fixing: sap uses the native pipe, so R CMD build raised Depends to R 4.1.0.
For Windows, a GitHub Actions matrix runs shiny-to-electron on macOS and Windows runners.
Building the .deb
The Linux build took the most work. I use Pop!_OS (an Ubuntu-flavored Linux distro), and shinyelectron will only build an .AppImage on Linux. bundled isn’t an option either, because there’s no portable R for Linux.
I found a fix by combining both chapters: The shinylive strategy runs sap with webR inside the Electron window, which uses the R-universe binary from the webR chapter. export() builds the .AppImage and leaves an Electron project behind. The build script then edits that project’s package.json and rebuilds it with electron-builder:
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'monospace', "darkMode":false}}}%%
flowchart TD
Start(["<code>electron/build.R</code>"])
Export("<code>export()</code> converts the app<br>to shinylive and builds Electron")
AppImage[/".AppImage"/]
Project[("<code>electron-app/</code><br>Electron project")]
Edit("Add the <code>deb</code> target,<br>homepage, and maintainer")
Builder("<code>electron-builder</code><br>packages the project")
Deb[/".deb"/]
Start -->|"i. export"| Export
Export --> AppImage
Export --> Project
Project -->|"ii. edit package.json"| Edit
Edit -->|"iii. npx electron-builder"| Builder
Builder --> Deb
style Start fill:#FFFFFF,color:#000000,stroke:#333
style Export fill:#4CBB9D,color:#FFFFFF,rx:5,ry:5
style Edit fill:#4CBB9D,color:#FFFFFF,rx:5,ry:5
style Builder fill:#4CBB9D,color:#FFFFFF,rx:5,ry:5
The edit adds three fields, because electron-builder refuses to build a .deb without a homepage and a maintainer:
pkg <- jsonlite::read_json(pkg_json)
pkg$homepage <- "https://github.com/mjfrigaard/sap"
pkg$build$linux$target <- list("AppImage", "deb")
pkg$build$linux$maintainer <- "Martin Frigaard <mjfrigaard@pm.me>"
jsonlite::write_json(pkg, pkg_json, auto_unbox = TRUE, pretty = TRUE)Pop!_OS ships Node.js 18, which is too old. install_nodejs() installs a newer version, and withr::with_path() puts it ahead of the system version before calling electron-builder.
After sudo dpkg -i, the 183 MB .deb adds Movie Reviews to the Applications launcher.
Recap
The two chapters take sap off the server. webR and shinylive run it in the browser, with a slower first load and code that every user can see. shinyelectron turns it into an installer, with bundled R on macOS and Windows and webR on Linux. Both start from the same launcher app.R, and neither changes the package itself.
webRchaptershinyelectronchaptersaprepo (branches22_webr,23.1_shinyelectron,23.2_shinyelectron-dmg, and23.3_shinyelectron-deb)shinyelectrondocs