├── .github ├── .gitignore └── workflows │ ├── deploy-app.yaml │ └── R-CMD-check.yaml ├── local ├── quarto │ ├── .gitignore │ ├── _quarto.yml │ └── shinylive.qmd └── shiny-apps │ ├── simple-r │ ├── about.txt │ └── app.R │ ├── global-r │ ├── global.R │ ├── server.R │ └── ui.R │ ├── simple-py │ └── app.py │ ├── issue-028-r │ └── app.R │ └── issue-029-r │ └── app.R ├── revdep ├── failures.md ├── problems.md ├── .gitignore ├── cran.md └── README.md ├── LICENSE ├── tests ├── testthat │ ├── apps │ │ ├── server-r │ │ │ ├── global.R │ │ │ ├── ui.R │ │ │ └── server.R │ │ ├── app-utf8 │ │ │ └── app.R │ │ ├── app-r │ │ │ └── app.R │ │ └── export_template │ │ │ ├── edit │ │ │ └── index.html │ │ │ └── index.html │ ├── test-assets.R │ ├── setup-skip.R │ ├── test-quarto_ext.R │ └── test-export.R ├── spelling.R └── testthat.R ├── .vscode ├── extensions.json └── settings.json ├── cran-comments.md ├── R ├── shinylive-package.R ├── version.R ├── cli.R ├── utils.R ├── app_json.R ├── export.R ├── deps.R ├── quarto_ext.R ├── assets.R └── packages.R ├── .gitignore ├── inst └── WORDLIST ├── NAMESPACE ├── .Rbuildignore ├── _pkgdown.yml ├── shinylive.Rproj ├── examples ├── deploy-app.yaml └── README.md ├── LICENSE.md ├── man ├── shinylive-package.Rd ├── install.Rd ├── assets.Rd ├── export.Rd └── quarto_ext.Rd ├── DESCRIPTION ├── NEWS.md └── README.md /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /local/quarto/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | -------------------------------------------------------------------------------- /revdep/failures.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /revdep/problems.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /local/shiny-apps/simple-r/about.txt: -------------------------------------------------------------------------------- 1 | Hello Shiny! 2 | -------------------------------------------------------------------------------- /local/quarto/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | title: "q" 3 | -------------------------------------------------------------------------------- /local/shiny-apps/global-r/global.R: -------------------------------------------------------------------------------- 1 | global_value <- 50 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2023 2 | COPYRIGHT HOLDER: shinylive authors 3 | -------------------------------------------------------------------------------- /tests/testthat/apps/server-r/global.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | global_value <- 50 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Posit.air-vscode" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /revdep/.gitignore: -------------------------------------------------------------------------------- 1 | checks 2 | library 3 | checks.noindex 4 | library.noindex 5 | cloud.noindex 6 | data.sqlite 7 | *.html 8 | -------------------------------------------------------------------------------- /tests/testthat/apps/app-utf8/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(utf8) 3 | 4 | ui <- fluidPage() 5 | server <- function(input, output) {} 6 | 7 | shinyApp(ui, server) 8 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Comments 2 | 3 | ## R CMD check results 4 | 5 | 0 errors | 0 warnings | 0 notes 6 | 7 | ## Reverse dependencies 8 | 9 | No reverse dependencies. 10 | -------------------------------------------------------------------------------- /tests/testthat/apps/server-r/ui.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | shinyUI(fluidPage( 4 | sliderInput("n", "N", 0, 100, 40), 5 | verbatimTextOutput("txt", placeholder = TRUE), 6 | )) 7 | -------------------------------------------------------------------------------- /local/shiny-apps/global-r/server.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | 4 | function(input, output) { 5 | output$txt <- renderText({ 6 | paste0("The value of n*2 is ", 2 * input$n) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /tests/spelling.R: -------------------------------------------------------------------------------- 1 | if (requireNamespace("spelling", quietly = TRUE)) { 2 | spelling::spell_check_test( 3 | vignettes = TRUE, 4 | error = FALSE, 5 | skip_on_cran = TRUE 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /R/shinylive-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | "_PACKAGE" 3 | 4 | ## usethis namespace: start 5 | #' @importFrom rlang %||% 6 | #' @importFrom rlang is_interactive 7 | ## usethis namespace: end 8 | NULL 9 | -------------------------------------------------------------------------------- /local/shiny-apps/global-r/ui.R: -------------------------------------------------------------------------------- 1 | fluidPage( 2 | markdown("## `ui.R` / `server.R` / `global.R`"), 3 | sliderInput("n", "N", 0, 100, global_value), 4 | verbatimTextOutput("txt", placeholder = TRUE), 5 | ) 6 | -------------------------------------------------------------------------------- /tests/testthat/apps/server-r/server.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | shinyServer(function(input, output, session) { 4 | output$txt <- renderText({ 5 | paste0("The value of n*2 is ", 2 * input$n) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /R/version.R: -------------------------------------------------------------------------------- 1 | # This is the version of the Shinylive assets to use. 2 | SHINYLIVE_ASSETS_VERSION <- "0.10.6" 3 | SHINYLIVE_R_VERSION <- as.character(utils::packageVersion("shinylive")) 4 | WEBR_R_VERSION <- "4.5.1" 5 | -------------------------------------------------------------------------------- /revdep/cran.md: -------------------------------------------------------------------------------- 1 | ## revdepcheck results 2 | 3 | We checked 0 reverse dependencies, comparing R CMD check results across CRAN and dev versions of this package. 4 | 5 | * We saw 0 new problems 6 | * We failed to check 0 packages 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | 3 | /.luarc.json 4 | barret* 5 | local/shiny-apps-out/ 6 | local/quarto/*.html 7 | local/quarto/*.html 8 | local/quarto/*_files/ 9 | local/quarto/*.js 10 | local/quarto/_extensions/ 11 | .Rproj.user 12 | venv/ 13 | -------------------------------------------------------------------------------- /tests/testthat/test-assets.R: -------------------------------------------------------------------------------- 1 | test_that("assets_dirs() contains files", { 2 | maybe_skip_test() 3 | 4 | assets_ensure() 5 | 6 | # Make sure we have assets installed 7 | expect_true(length(assets_dirs()) > 0) 8 | expect_true(length(dir(assets_dirs()[1])) > 0) 9 | }) 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[r]": { 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "Posit.air-vscode" 5 | }, 6 | "[quarto]": { 7 | "editor.formatOnSave": true, 8 | "editor.defaultFormatter": "quarto.quarto" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | Buie 2 | CLI 3 | CMD 4 | JS 5 | JSON 6 | ORCID 7 | PBC 8 | WebAssembly 9 | api 10 | dev 11 | funder 12 | github 13 | https 14 | io 15 | pre 16 | precompiled 17 | py 18 | pyodide 19 | pyright 20 | repo 21 | stdout 22 | stylesheets 23 | symlink 24 | toolchain 25 | usethis 26 | webR 27 | webr 28 | -------------------------------------------------------------------------------- /local/shiny-apps/simple-r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | shinyApp( 4 | fluidPage( 5 | sliderInput("n", "N", 0, 100, 40), 6 | verbatimTextOutput("txt", placeholder = TRUE), 7 | ), 8 | function(input, output) { 9 | output$txt <- renderText({ 10 | paste0("The value of n*2 is ", 2 * input$n) 11 | }) 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /tests/testthat/apps/app-r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | ui <- fluidPage( 4 | sliderInput("n", "N", 0, 100, 40), 5 | verbatimTextOutput("txt", placeholder = TRUE), 6 | ) 7 | 8 | server <- function(input, output) { 9 | output$txt <- renderText({ 10 | paste0("The value of n*2 is ", 2 * input$n) 11 | }) 12 | } 13 | 14 | shinyApp(ui, server) 15 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(assets_cleanup) 4 | export(assets_download) 5 | export(assets_ensure) 6 | export(assets_info) 7 | export(assets_install_copy) 8 | export(assets_install_link) 9 | export(assets_remove) 10 | export(assets_version) 11 | export(export) 12 | importFrom(rlang,"%||%") 13 | importFrom(rlang,is_interactive) 14 | -------------------------------------------------------------------------------- /tests/testthat/apps/export_template/edit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redirect to editable app 5 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^LICENSE\.md$ 2 | ^_pkgdown\.yml$ 3 | ^docs$ 4 | ^pkgdown$ 5 | ^barret 6 | ^local$ 7 | ^\.luarc\.json$ 8 | ^.github$ 9 | ^shinylive\.Rproj$ 10 | ^\.github$ 11 | ^shinylive_assets$ 12 | ^cran-comments\.md$ 13 | ^revdep$ 14 | ^CRAN-SUBMISSION$ 15 | ^examples$ 16 | ^\.Rproj\.user$ 17 | ^venv$ 18 | ^.venv$ 19 | ^_dev$ 20 | ^node_modules$ 21 | ^[.]?air[.]toml$ 22 | ^\.vscode$ 23 | -------------------------------------------------------------------------------- /local/shiny-apps/simple-py/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_slider("n", "N", 0, 100, 20), 5 | ui.output_text_verbatim("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @output 11 | @render.text 12 | def txt(): 13 | return f"n*2 is {input.n() * 2}" 14 | 15 | 16 | app = App(app_ui, server) 17 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://posit-dev.github.io/r-shinylive/ 2 | 3 | development: 4 | mode: auto 5 | 6 | template: 7 | package: tidytemplate 8 | bootstrap: 5 9 | 10 | bslib: 11 | primary: "#007BC2" 12 | navbar-background: "#dff3ff" 13 | # navbar-background: "#007BC2" 14 | # navbar-color: "#FFFFFF" 15 | # primary: "#2380c2" 16 | # navbar-background: "#e7f3fb" 17 | trailing_slash_redirect: true 18 | -------------------------------------------------------------------------------- /tests/testthat/setup-skip.R: -------------------------------------------------------------------------------- 1 | can_test_assets <- function() { 2 | isTRUE(as.logical(Sys.getenv("TEST_ASSETS", "false"))) 3 | } 4 | is_interative_or_on_ci <- function() { 5 | interactive() || can_test_assets() 6 | } 7 | maybe_skip_test <- function() { 8 | skip_on_cran() 9 | skip_if( 10 | !is_interative_or_on_ci(), 11 | "Skipping test on non-interactive session. To run this test, set environment variable TEST_ASSETS=1." 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview 7 | # * https://testthat.r-lib.org/articles/special-files.html 8 | 9 | library(testthat) 10 | library(shinylive) 11 | 12 | test_check("shinylive") 13 | -------------------------------------------------------------------------------- /shinylive.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: No 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /local/shiny-apps/issue-028-r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | ui <- fluidPage(verbatimTextOutput("search_values")) 4 | 5 | server <- function(input, output, session) { 6 | observe({ 7 | str(reactiveValuesToList(session$clientData)) 8 | }) 9 | 10 | output$search_values <- renderText({ 11 | invalidateLater(1000) 12 | paste( 13 | capture.output({ 14 | print(Sys.time()) 15 | str(getQueryString()) 16 | }), 17 | collapse = "\n" 18 | ) 19 | }) 20 | } 21 | 22 | shinyApp(ui, server) 23 | -------------------------------------------------------------------------------- /local/shiny-apps/issue-029-r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | ui <- fluidPage( 4 | selectInput("dataset", "Choose a dataset", c("pressure", "cars")), 5 | selectInput("column", "Choose column", character(0)), 6 | verbatimTextOutput("summary") 7 | ) 8 | 9 | server <- function(input, output, session) { 10 | dataset <- reactive(get(input$dataset, "package:datasets")) 11 | 12 | observeEvent(input$dataset, { 13 | freezeReactiveValue(input, "column") 14 | updateSelectInput(inputId = "column", choices = names(dataset())) 15 | }) 16 | 17 | output$summary <- renderPrint({ 18 | summary(dataset()[[input$column]]) 19 | }) 20 | } 21 | 22 | shinyApp(ui, server) 23 | -------------------------------------------------------------------------------- /revdep/README.md: -------------------------------------------------------------------------------- 1 | # Platform 2 | 3 | |field |value | 4 | |:--------|:------------------------------| 5 | |version |R version 4.3.0 (2023-04-21) | 6 | |os |macOS Ventura 13.5.1 | 7 | |system |aarch64, darwin20 | 8 | |ui |X11 | 9 | |language |(EN) | 10 | |collate |en_US.UTF-8 | 11 | |ctype |en_US.UTF-8 | 12 | |tz |America/New_York | 13 | |date |2023-09-12 | 14 | |pandoc |3.1 @ /opt/homebrew/bin/pandoc | 15 | 16 | # Dependencies 17 | 18 | |package |old |new |Δ | 19 | |:---------|:---|:----------|:--| 20 | |shinylive |NA |0.0.0.9000 |* | 21 | 22 | # Revdeps 23 | 24 | -------------------------------------------------------------------------------- /examples/deploy-app.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/posit-dev/r-shinylive/tree/actions-v1/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # Basic example of a GitHub Actions workflow that builds a Shiny app and deploys 5 | # it to GitHub Pages. 6 | name: Deploy app to gh-pages 7 | 8 | on: 9 | # Manually trigger the workflow 10 | workflow_dispatch: 11 | # Trigger on push to `main` branch 12 | push: 13 | branches: ["main"] 14 | # Trigger on pull request to all branches (but do not deploy to gh-pages) 15 | pull_request: 16 | 17 | jobs: 18 | shinylive: 19 | uses: posit-dev/r-shinylive/.github/workflows/deploy-app.yaml@actions-v1 20 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 21 | permissions: 22 | pages: write # to deploy to Pages 23 | id-token: write # to verify the deployment originates from an appropriate source 24 | -------------------------------------------------------------------------------- /tests/testthat/apps/export_template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | 8 | 12 | 20 | 21 | 22 | {{{ include_in_head }}} 23 | 24 | 25 | {{{ include_before_body }}} 26 |
27 | {{{ include_after_body }}} 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 shinylive authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /local/quarto/shinylive.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Shinylive applications embedded in Quarto documents 3 | format: html 4 | filters: 5 | - quarto-ext/shinylive 6 | --- 7 | 8 | # `R` 9 | 10 | :::{.column-page-inset-right} 11 | ```{shinylive-r} 12 | #| standalone: true 13 | #| components: [editor, viewer] 14 | library(shiny) 15 | 16 | shinyApp( 17 | fluidPage( 18 | sliderInput("n", "N", 0, 100, 20), 19 | verbatimTextOutput("txt", placeholder = TRUE), 20 | ), 21 | function(input, output) { 22 | output$txt <- renderText({ 23 | paste0("n*2 is ", 2 * input$n) 24 | }) 25 | } 26 | ) 27 | ``` 28 | ::: 29 | 30 | # `python` 31 | 32 | :::{.column-page-inset-right} 33 | ```{shinylive-python} 34 | #| standalone: true 35 | #| components: [editor, viewer] 36 | from shiny import App, render, ui 37 | 38 | app_ui = ui.page_fluid( 39 | ui.input_slider("n", "N", 0, 100, 20), 40 | ui.output_text_verbatim("txt"), 41 | ) 42 | 43 | 44 | def server(input, output, session): 45 | @output 46 | @render.text 47 | def txt(): 48 | return f"n*2 is {2 * input.n()}" 49 | 50 | 51 | app = App(app_ui, server) 52 | ``` 53 | ::: 54 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Example workflows 2 | 3 | Package workflows: 4 | 5 | - [`deploy-app`](#deploy-app) - A simple CI workflow to 6 | check with the release version of R. 7 | 8 | ## Deploy App 9 | 10 | Reusable workflow that will deploy the root Shiny app dir to GitHub pages. 11 | 12 | The agreed upon contract is: 13 | 14 | - Inspect the root directory for package dependencies 15 | - Install R and the found packages 16 | - Export the Shiny app directory to `./site` 17 | - On push events, deploy the exported app to GitHub Pages 18 | 19 | If this contract is not met or could be easily improved for others, please open 20 | a new Issue https://github.com/posit-dev/r-shinylive/ . 21 | 22 | To add the workflow to your repository, call `usethis::use_github_action(url="https://github.com/posit-dev/r-shinylive/blob/actions-v1/examples/deploy-app.yaml")`. 23 | 24 | 25 | # Contributing 26 | 27 | If any changes are made to the reusable workflows in `.github/workflows/`, please force update the tag `actions-v1` to the latest appropriate git sha. This will allow users to easily reference the latest version of the workflow. 28 | 29 | ```bash 30 | git tag -f actions-v1 31 | git push --tags 32 | ``` 33 | 34 | This update is not necessary for changes in `examples/` as these files are copied within each of the user's repositories. 35 | -------------------------------------------------------------------------------- /man/shinylive-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/shinylive-package.R 3 | \docType{package} 4 | \name{shinylive-package} 5 | \alias{shinylive} 6 | \alias{shinylive-package} 7 | \title{shinylive: Run 'shiny' Applications in the Browser} 8 | \description{ 9 | Exporting 'shiny' applications with 'shinylive' allows you to run them entirely in a web browser, without the need for a separate R server. The traditional way of deploying 'shiny' applications involves in a separate server and client: the server runs R and 'shiny', and clients connect via the web browser. When an application is deployed with 'shinylive', R and 'shiny' run in the web browser (via 'webR'): the browser is effectively both the client and server for the application. This allows for your 'shiny' application exported by 'shinylive' to be hosted by a static web server. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://posit-dev.github.io/r-shinylive/} 15 | \item \url{https://github.com/posit-dev/r-shinylive} 16 | \item Report bugs at \url{https://github.com/posit-dev/r-shinylive/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Barret Schloerke \email{barret@posit.co} (\href{https://orcid.org/0000-0001-9986-114X}{ORCID}) 22 | 23 | Authors: 24 | \itemize{ 25 | \item Winston Chang \email{winston@posit.co} (\href{https://orcid.org/0000-0002-1576-2126}{ORCID}) 26 | \item George Stagg \email{george.stagg@posit.co} 27 | \item Garrick Aden-Buie \email{garrick@posit.co} (\href{https://orcid.org/0000-0002-7111-0077}{ORCID}) 28 | } 29 | 30 | Other contributors: 31 | \itemize{ 32 | \item Posit Software, PBC (\href{https://ror.org/03wc8by49}{ROR}) [copyright holder, funder] 33 | } 34 | 35 | } 36 | \keyword{internal} 37 | -------------------------------------------------------------------------------- /man/install.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/assets.R 3 | \name{assets_install_copy} 4 | \alias{assets_install_copy} 5 | \alias{assets_install_link} 6 | \title{Install shinylive assets from from a local directory} 7 | \usage{ 8 | assets_install_copy( 9 | assets_repo_dir, 10 | ..., 11 | dir = assets_cache_dir(), 12 | version = package_json_version(assets_repo_dir) 13 | ) 14 | 15 | assets_install_link( 16 | assets_repo_dir, 17 | ..., 18 | dir = assets_cache_dir(), 19 | version = package_json_version(assets_repo_dir) 20 | ) 21 | } 22 | \arguments{ 23 | \item{assets_repo_dir}{The local repository directory for shinylive assets 24 | (e.g. \href{https://github.com/posit-dev/py-shinylive}{\code{posit-dev/shinylive}})} 25 | 26 | \item{...}{Ignored.} 27 | 28 | \item{dir}{The asset cache directory. Unless testing, the default behavior 29 | should be used.} 30 | 31 | \item{version}{The version of the assets being installed.} 32 | } 33 | \value{ 34 | All method return \code{invisible()}. 35 | } 36 | \description{ 37 | Helper methods for testing updates to shinylive assets. 38 | } 39 | \section{Functions}{ 40 | \itemize{ 41 | \item \code{assets_install_copy()}: Copies all shinylive assets from a local shinylive 42 | repository (e.g. 43 | \href{https://github.com/posit-dev/py-shinylive}{\code{posit-dev/shinylive}}). This 44 | must be repeated for any change in the assets. 45 | 46 | \item \code{assets_install_link()}: Creates a symlink of the local shinylive assets to the 47 | cached assets directory. After the first installation, the assets will the 48 | same as the source due to the symlink. 49 | 50 | }} 51 | \seealso{ 52 | \code{\link[=assets_download]{assets_download()}}, \code{\link[=assets_ensure]{assets_ensure()}}, \code{\link[=assets_cleanup]{assets_cleanup()}} 53 | } 54 | -------------------------------------------------------------------------------- /R/cli.R: -------------------------------------------------------------------------------- 1 | local_quiet <- function(quiet = FALSE, .envir = parent.frame()) { 2 | withr::local_options(list(shinylive.quiet = quiet), .local_envir = .envir) 3 | } 4 | 5 | is_quiet <- function() { 6 | isTRUE(getOption("shinylive.quiet", FALSE)) 7 | } 8 | 9 | cli_alert <- function(..., .envir = parent.frame()) { 10 | if (is_quiet()) { 11 | return(invisible()) 12 | } 13 | cli::cli_alert(..., .envir = .envir) 14 | } 15 | 16 | cli_alert_info <- function(..., .envir = parent.frame()) { 17 | if (is_quiet()) { 18 | return(invisible()) 19 | } 20 | cli::cli_alert_info(..., .envir = .envir) 21 | } 22 | 23 | cli_alert_warning <- function(..., .envir = parent.frame()) { 24 | if (is_quiet()) { 25 | return(invisible()) 26 | } 27 | cli::cli_alert_warning(..., .envir = .envir) 28 | } 29 | 30 | cli_alert_danger <- function(..., .envir = parent.frame()) { 31 | if (is_quiet()) { 32 | return(invisible()) 33 | } 34 | cli::cli_alert_danger(..., .envir = .envir) 35 | } 36 | 37 | cli_alert_success <- function(..., .envir = parent.frame()) { 38 | if (is_quiet()) { 39 | return(invisible()) 40 | } 41 | cli::cli_alert_success(..., .envir = .envir) 42 | } 43 | 44 | cli_progress_step <- function(..., .envir = parent.frame()) { 45 | if (is_quiet()) { 46 | return(invisible()) 47 | } 48 | cli::cli_progress_step(..., .envir = .envir) 49 | } 50 | 51 | cli_progress_done <- function(..., .envir = parent.frame()) { 52 | if (is_quiet()) { 53 | return(invisible()) 54 | } 55 | cli::cli_progress_done(..., .envir = .envir) 56 | } 57 | 58 | cli_text <- function(..., .envir = parent.frame()) { 59 | if (is_quiet()) { 60 | return(invisible()) 61 | } 62 | cli::cli_text(..., .envir = .envir) 63 | } 64 | 65 | cli_bullets <- function(..., .envir = parent.frame()) { 66 | if (is_quiet()) { 67 | return(invisible()) 68 | } 69 | cli::cli_bullets(..., .envir = .envir) 70 | } 71 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: shinylive 2 | Title: Run 'shiny' Applications in the Browser 3 | Version: 0.3.0.9000 4 | Authors@R: c( 5 | person("Barret", "Schloerke", , "barret@posit.co", role = c("aut", "cre"), 6 | comment = c(ORCID = "0000-0001-9986-114X")), 7 | person("Winston", "Chang", , "winston@posit.co", role = "aut", 8 | comment = c(ORCID = "0000-0002-1576-2126")), 9 | person("George", "Stagg", , "george.stagg@posit.co", role = "aut"), 10 | person("Garrick", "Aden-Buie", , "garrick@posit.co", role = "aut", 11 | comment = c(ORCID = "0000-0002-7111-0077")), 12 | person("Posit Software, PBC", role = c("cph", "fnd"), 13 | comment = c(ROR = "03wc8by49")) 14 | ) 15 | Description: Exporting 'shiny' applications with 'shinylive' allows you to 16 | run them entirely in a web browser, without the need for a separate R 17 | server. The traditional way of deploying 'shiny' applications involves 18 | in a separate server and client: the server runs R and 'shiny', and 19 | clients connect via the web browser. When an application is deployed 20 | with 'shinylive', R and 'shiny' run in the web browser (via 'webR'): 21 | the browser is effectively both the client and server for the 22 | application. This allows for your 'shiny' application exported by 23 | 'shinylive' to be hosted by a static web server. 24 | License: MIT + file LICENSE 25 | URL: https://posit-dev.github.io/r-shinylive/, 26 | https://github.com/posit-dev/r-shinylive 27 | BugReports: https://github.com/posit-dev/r-shinylive/issues 28 | Imports: 29 | archive, 30 | brio, 31 | cli, 32 | fs, 33 | gh, 34 | glue, 35 | httr2 (>= 1.0.0), 36 | jsonlite, 37 | pkgdepends, 38 | rappdirs, 39 | renv, 40 | rlang, 41 | tools, 42 | whisker, 43 | withr 44 | Suggests: 45 | httpuv (>= 1.6.12), 46 | pkgcache, 47 | spelling, 48 | testthat (>= 3.0.0) 49 | Config/Needs/website: tidyverse/tidytemplate 50 | Config/testthat/edition: 3 51 | Encoding: UTF-8 52 | Language: en-US 53 | Roxygen: list(markdown = TRUE) 54 | RoxygenNote: 7.3.3 55 | -------------------------------------------------------------------------------- /man/assets.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/assets.R 3 | \name{assets_download} 4 | \alias{assets_download} 5 | \alias{assets_ensure} 6 | \alias{assets_cleanup} 7 | \alias{assets_remove} 8 | \alias{assets_info} 9 | \alias{assets_version} 10 | \title{Manage shinylive assets} 11 | \usage{ 12 | assets_download( 13 | version = assets_version(), 14 | ..., 15 | dir = assets_cache_dir(), 16 | url = assets_bundle_url(version) 17 | ) 18 | 19 | assets_ensure( 20 | version = assets_version(), 21 | ..., 22 | dir = assets_cache_dir(), 23 | url = assets_bundle_url(version) 24 | ) 25 | 26 | assets_cleanup(..., dir = assets_cache_dir()) 27 | 28 | assets_remove(versions, ..., dir = assets_cache_dir()) 29 | 30 | assets_info(quiet = FALSE) 31 | 32 | assets_version() 33 | } 34 | \arguments{ 35 | \item{version}{The version of the assets to download.} 36 | 37 | \item{...}{Ignored.} 38 | 39 | \item{dir}{The asset cache directory. Unless testing, the default behavior 40 | should be used.} 41 | 42 | \item{url}{The URL to download the assets from. Unless testing, the default 43 | behavior should be used.} 44 | 45 | \item{versions}{The assets versions to remove.} 46 | 47 | \item{quiet}{In \code{assets_info()}, if \code{quiet = TRUE}, the function will not 48 | print the assets information to the console.} 49 | } 50 | \value{ 51 | \code{assets_version()} returns the version of the currently supported Shinylive. 52 | 53 | All other methods return \code{invisible()}. 54 | } 55 | \description{ 56 | Helper methods for managing shinylive assets. 57 | } 58 | \section{Functions}{ 59 | \itemize{ 60 | \item \code{assets_download()}: Downloads the shinylive assets bundle from GitHub and 61 | extracts it to the specified directory. The bundle will always be 62 | downloaded from GitHub, even if it already exists in the cache directory 63 | (\verb{dir=}). 64 | 65 | \item \code{assets_ensure()}: Ensures a local copy of shinylive is installed. If a local 66 | copy of shinylive is not installed, it will be downloaded and installed. 67 | If a local copy of shinylive is installed, its path will be returned. 68 | 69 | \item \code{assets_cleanup()}: Removes local copies of shinylive web assets, except for 70 | the one used by the current version of \pkg{shinylive}. 71 | 72 | \item \code{assets_remove()}: Removes a local copies of shinylive web assets. 73 | 74 | \item \code{assets_info()}: Prints information about the local shinylive assets that 75 | have been installed. Invisibly returns a table of installed asset versions 76 | and their associated paths. 77 | 78 | \item \code{assets_version()}: Returns the version of the currently supported Shinylive 79 | assets version. If the \code{SHINYLIVE_ASSETS_VERSION} environment variable is set, 80 | that value will be used. 81 | 82 | }} 83 | -------------------------------------------------------------------------------- /.github/workflows/deploy-app.yaml: -------------------------------------------------------------------------------- 1 | # Basic example of a GitHub Actions workflow that builds a Shiny app and deploys 2 | # it to GitHub Pages. 3 | # 4 | # The agreed upon contract is: 5 | # 6 | # - Inspect the root directory for package dependencies 7 | # - Install R and the found packages 8 | # - Export the Shiny app directory to `./site` 9 | # - On push events, deploy the exported app to GitHub Pages 10 | # 11 | # If this contract is not met or could be easily improved for others, 12 | # please open a new Issue https://github.com/posit-dev/r-shinylive/ 13 | # 14 | # The _magic_ of this workflow is in the `shinylive::export()` function, which 15 | # creates a static version of the Shiny app into the folder `./site`. 16 | # The exported app folder is then uploaded and deployed to GitHub Pages. 17 | # 18 | # When deploying to GitHub Pages, be sure to have the appropriate write 19 | # permissions for your token (`pages` and `id-token`). 20 | 21 | name: Deploy app 22 | 23 | on: 24 | workflow_call: 25 | inputs: 26 | cache-version: 27 | type: string 28 | default: "1" 29 | required: false 30 | 31 | jobs: 32 | build: 33 | runs-on: ubuntu-latest 34 | env: 35 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 36 | R_KEEP_PKG_SOURCE: yes 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - uses: rstudio/shiny-workflows/setup-r-package@v1 42 | with: 43 | packages: | 44 | renv 45 | posit-dev/r-shinylive 46 | sessioninfo 47 | cache-version: ${{ github.event.inputs.cache-version }} 48 | 49 | - name: Find package dependencies 50 | shell: Rscript {0} 51 | id: packages 52 | run: | 53 | # Find package dependencies using {renv} and install with {pak} 54 | pak::pak( 55 | unique(renv::dependencies(".")$Package) 56 | ) 57 | 58 | - name: Build site 59 | shell: Rscript {0} 60 | run: | 61 | shinylive::export(".", "site") 62 | 63 | - name: Upload site artifact 64 | if: github.ref == 'refs/heads/main' 65 | uses: actions/upload-pages-artifact@v3 66 | with: 67 | path: "site" 68 | 69 | deploy: 70 | if: github.ref == 'refs/heads/main' 71 | needs: build 72 | 73 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 74 | permissions: 75 | pages: write # to deploy to Pages 76 | id-token: write # to verify the deployment originates from an appropriate source 77 | 78 | # Deploy to the github-pages environment 79 | environment: 80 | name: github-pages 81 | url: ${{ steps.deployment.outputs.page_url }} 82 | 83 | # Specify runner + deployment step 84 | runs-on: ubuntu-latest 85 | steps: 86 | - name: Deploy to GitHub Pages 87 | id: deployment 88 | uses: actions/deploy-pages@v4 89 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # shinylive (development version) 2 | 3 | * Updated default shinylive assets to [v0.10.6](https://github.com/posit-dev/shinylive/releases/tag/v0.10.6). (#165, #166) 4 | 5 | # shinylive 0.3.0 6 | 7 | * Updated default shinylive assets to [v0.9.1](https://github.com/posit-dev/shinylive/releases/tag/v0.9.1). (#120, #129, #135) 8 | 9 | * Resources are now built relative to Quarto project root. (#130) 10 | 11 | * In CI and other automated workflow settings the `SHINYLIVE_WASM_PACKAGES` environment variable can now be used to control whether WebAssembly R package binaries are bundled with the exported shinylive app, in addition to the `wasm_packages` argument of the `export()` function. (#116) 12 | 13 | * shinylive now avoids bundling WebAssembly R package dependencies listed only in the `LinkingTo` section of required packages. With this change dependencies that are only required at build time are no longer included as part of the exported WebAssembly asset bundle. This reduces the total static asset size and improves the loading time of affected shinylive apps. (#115) 14 | 15 | * shinylive now supports adding files in virtual subdirectories in `shinylive-r` apps embedded in Quarto documents. For example, `## file: R/load_data.R` in a `shinylive-r` chunk followed by the `load_data.R` code will create a file `load_data.R` in the `R` subdirectory of the exported app. (#119) 16 | 17 | # shinylive 0.2.0 18 | 19 | * shinylive now uses [shinylive web assets v0.5.0](https://github.com/posit-dev/shinylive/releases/tag/v0.5.0) by default, which bundles webR 0.4.0 with R 4.4.1. This update brings improved keyboard shortcuts for R users in the Shinylive editor, the ability to export a custom library of R packages with the exported app, and a few improvements to the Quarto integration. (#108) 20 | 21 | * `export()` gains an `assets_version` argument to choose the version of the Shinylive web assets to be used with the exported app. This is primarily useful for testing new versions of the Shinylive assets before they're officially released via a package update. In CI and other automated workflow settings, the `SHINYLIVE_ASSETS_VERSION` environment variable can be used to set the assets version. (#91) 22 | 23 | * `export()` gains `template_params` and `template_dir` arguments to control the template HTML files used in the export, allowing users to partially or completely customize the exported HTML. The export template is provided by the shinylive assets and may change from release-to-release. Use `assets_info()` to locate installed shinylive assets; the template files for a given release are in the `export_template` directory of the release. (#96) 24 | * `template_params` takes a list of parameters to be interpolated into the template. The default template include `title` (the title for the page with the exported app), `include_in_head` (HTML added to the `` of the page), and `include_before_body` (HTML added just after ``) and `include_after_body` (HTML added just after ``). 25 | * `template_dir` is the directory containing the template files. The default is the `export_template` directory of the shinylive assets being used for the export. Use `assets_info()` to locate installed shinylive assets where you can find the default template files. 26 | 27 | * shinylive now uses `{cli}` for console printing. Console output can be suppressed via the global R option by calling `options(shinylive.quiet = TRUE)`. (#104) 28 | 29 | * `export()` and `assets_info()` gain a `quiet` argument. In `export()`, `quiet` replaces the now-deprecated `verbose` option, which continues to work with a warning. (#104) 30 | 31 | # shinylive 0.1.1 32 | 33 | * Bump shinylive assets dependency to 0.2.3. (#38) 34 | 35 | * Use `{httpuv}` to serve static folder instead of plumber. (#40) 36 | 37 | * Use `{httr2}` to download assets from GitHub releases. (@dgkf #30, #39) 38 | 39 | # shinylive 0.1.0 40 | 41 | * Initial CRAN submission. 42 | -------------------------------------------------------------------------------- /man/export.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/export.R 3 | \name{export} 4 | \alias{export} 5 | \title{Export a Shiny app to a directory} 6 | \usage{ 7 | export( 8 | appdir, 9 | destdir, 10 | ..., 11 | subdir = "", 12 | quiet = getOption("shinylive.quiet", !is_interactive()), 13 | wasm_packages = NULL, 14 | package_cache = TRUE, 15 | max_filesize = NULL, 16 | assets_version = NULL, 17 | template_dir = NULL, 18 | template_params = list(), 19 | verbose = NULL 20 | ) 21 | } 22 | \arguments{ 23 | \item{appdir}{Directory containing the application.} 24 | 25 | \item{destdir}{Destination directory.} 26 | 27 | \item{...}{Ignored} 28 | 29 | \item{subdir}{Subdirectory of \code{destdir} to write the app to.} 30 | 31 | \item{quiet}{Suppress console output during export. Follows the global 32 | \code{shinylive.quiet} option or defaults to \code{FALSE} in interactive sessions if 33 | not set.} 34 | 35 | \item{wasm_packages}{Download and include binary WebAssembly packages as part 36 | of the output app's static assets. Logical, defaults to \code{TRUE}. The default 37 | value can be changed by setting the environment variable 38 | \code{SHINYLIVE_WASM_PACKAGES} to \code{TRUE} or \code{1} to enable, \code{FALSE} or \code{0} to 39 | disable.} 40 | 41 | \item{package_cache}{Cache downloaded binary WebAssembly packages. Defaults 42 | to \code{TRUE}.} 43 | 44 | \item{max_filesize}{Maximum file size for bundling of WebAssembly package 45 | assets. Parsed by \code{\link[fs:fs_bytes]{fs::fs_bytes()}}. Defaults to \code{"100M"}. The default 46 | value can be changed by setting the environment variable 47 | \code{SHINYLIVE_DEFAULT_MAX_FILESIZE}. Set to \code{Inf}, \code{NA} or \code{-1} to disable.} 48 | 49 | \item{assets_version}{The version of the Shinylive assets to use in the 50 | exported app. Defaults to \code{\link[=assets_version]{assets_version()}}. Note, not all custom assets 51 | versions may work with this release of \pkg{shinylive}. Please visit the 52 | \href{https://github.com/posit-dev/shinylive/releases}{shinylive asset releases} 53 | website to learn more information about the available \code{assets_version} 54 | values.} 55 | 56 | \item{template_dir}{Path to a custom template directory to use when exporting 57 | the shinylive app. The template can be copied from the shinylive assets 58 | using: \code{fs::path(shinylive:::assets_dir(), "export_template")}.} 59 | 60 | \item{template_params}{A list of parameters to pass to the template. The 61 | supported parameters depends on the template being used. Custom templates 62 | may support additional parameters (see \code{template_dir} for instructions on 63 | creating a custom template or to find the current shinylive assets' 64 | templates). 65 | 66 | With shinylive assets > 0.4.1, the default export template supports the 67 | following parameters: 68 | \enumerate{ 69 | \item \code{title}: The title of the app. Defaults to \code{"Shiny app"}. 70 | \item \code{include_in_head}, \code{include_before_body}, \code{include_after_body}: Raw 71 | HTML to be included in the \verb{}, just after the opening \verb{}, 72 | or just before the closing \verb{} tag, respectively. 73 | }} 74 | 75 | \item{verbose}{Deprecated, please use \code{quiet} instead.} 76 | } 77 | \value{ 78 | Nothing. The app is exported to \code{destdir}. Instructions for serving 79 | the directory are printed to stdout. 80 | } 81 | \description{ 82 | This function exports a Shiny app to a directory, which can then be served 83 | using \code{httpuv}. 84 | } 85 | \examples{ 86 | \dontshow{if (rlang::is_interactive()) withAutoprint(\{ # examplesIf} 87 | app_dir <- system.file("examples", "01_hello", package = "shiny") 88 | out_dir <- tempfile("shinylive-export") 89 | 90 | # Export the app to a directory 91 | export(app_dir, out_dir) 92 | 93 | # Serve the exported directory 94 | if (require(httpuv)) { 95 | httpuv::runStaticServer(out_dir) 96 | } 97 | \dontshow{\}) # examplesIf} 98 | } 99 | -------------------------------------------------------------------------------- /tests/testthat/test-quarto_ext.R: -------------------------------------------------------------------------------- 1 | test_that("quarto_ext handles `extension info`", { 2 | maybe_skip_test() 3 | 4 | assets_ensure() 5 | 6 | txt <- collapse(capture.output({ 7 | quarto_ext(c("extension", "info")) 8 | })) 9 | info <- jsonlite::parse_json(txt) 10 | expect_equal(info$version, as.character(utils::packageVersion("shinylive"))) 11 | expect_equal(info$assets_version, assets_version()) 12 | 13 | expect_true( 14 | is.list(info$scripts) && 15 | length(info$scripts) == 1 && 16 | nzchar(info$scripts$`codeblock-to-json`) 17 | ) 18 | expect_equal( 19 | info$scripts$`codeblock-to-json`, 20 | quarto_codeblock_to_json_path() 21 | ) 22 | }) 23 | 24 | 25 | test_that("quarto_ext handles `extension base-htmldeps`", { 26 | maybe_skip_test() 27 | 28 | assets_ensure() 29 | 30 | txt <- collapse(capture.output({ 31 | quarto_ext(c("extension", "base-htmldeps", "--sw-dir", "TEST_PATH_SW_DIR")) 32 | })) 33 | items <- jsonlite::parse_json(txt) 34 | expect_length(items, 2) 35 | 36 | worker_item <- items[[1]] 37 | expect_equal( 38 | worker_item$meta[["shinylive:serviceworker_dir"]], 39 | "TEST_PATH_SW_DIR" 40 | ) 41 | 42 | shinylive_item <- items[[2]] 43 | # Verify there is a quarto html dependency with name `"shinylive"` 44 | expect_equal(shinylive_item$name, "shinylive") 45 | # Verify webr can NOT be found in resources 46 | shinylive_resources <- shinylive_item$resources 47 | expect_false(any(grepl( 48 | "webr", 49 | vapply(shinylive_resources, `[[`, character(1), "name"), 50 | fixed = TRUE 51 | ))) 52 | }) 53 | test_that("quarto_ext handles `extension language-resources`", { 54 | maybe_skip_test() 55 | 56 | assets_ensure() 57 | 58 | txt <- collapse(capture.output({ 59 | quarto_ext(c("extension", "language-resources")) 60 | })) 61 | resources <- jsonlite::parse_json(txt) 62 | 63 | # Verify webr folder in path 64 | expect_true(any(grepl( 65 | "webr", 66 | vapply(resources, `[[`, character(1), "name"), 67 | fixed = TRUE 68 | ))) 69 | }) 70 | 71 | 72 | test_that("quarto_ext handles `extension app-resources`", { 73 | maybe_skip_test() 74 | 75 | assets_ensure() 76 | 77 | # Clean-up on exit 78 | tmpdir <- tempdir() 79 | wd <- setwd(tmpdir) 80 | on.exit({ 81 | setwd(wd) 82 | fs::dir_delete(tmpdir) 83 | }) 84 | 85 | app_json <- '[{"name":"app.R","type":"text","content":"library(shiny)"}]' 86 | writeLines(app_json, "app.json") 87 | 88 | txt <- collapse(capture.output({ 89 | quarto_ext(c("extension", "app-resources"), con = "app.json") 90 | })) 91 | resources <- jsonlite::parse_json(txt) 92 | 93 | # Package metadata included in resources 94 | expect_true(any(grepl( 95 | "metadata.rds", 96 | vapply(resources, `[[`, character(1), "name"), 97 | fixed = TRUE 98 | ))) 99 | }) 100 | 101 | test_that("quarto_ext handles `extension app-resources` with additional binary files", { 102 | maybe_skip_test() 103 | 104 | assets_ensure() 105 | 106 | # Clean-up on exit 107 | tmpdir <- tempdir() 108 | wd <- setwd(tmpdir) 109 | on.exit({ 110 | setwd(wd) 111 | fs::dir_delete(tmpdir) 112 | }) 113 | 114 | # A binary file included in app.json should successfully be decoded while 115 | # building package metadata for app-resources. 116 | app_json <- '[{"name":"app.R","type":"text","content":"library(shiny)"},{"name":"image.png","type":"binary","content":"iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAAA1BMVEW10NBjBBbqAAAAH0lEQVRoge3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAvg0hAAABmmDh1QAAAABJRU5ErkJggg=="}]' 117 | writeLines(app_json, "app.json") 118 | 119 | txt <- collapse(capture.output({ 120 | quarto_ext(c("extension", "app-resources"), con = "app.json") 121 | })) 122 | resources <- jsonlite::parse_json(txt) 123 | 124 | # Package metadata included in resources 125 | expect_true(any(grepl( 126 | "metadata.rds", 127 | vapply(resources, `[[`, character(1), "name"), 128 | fixed = TRUE 129 | ))) 130 | }) 131 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | assert_nzchar_string <- function(x) { 2 | stopifnot(is.character(x) && nchar(x) > 0) 3 | invisible(TRUE) 4 | } 5 | assert_list_items <- function(x, item_class) { 6 | stopifnot(is.list(x) && all(vapply(x, inherits, logical(1), item_class))) 7 | invisible(TRUE) 8 | } 9 | assert_list <- function(x) { 10 | stopifnot(is.list(x)) 11 | invisible(TRUE) 12 | } 13 | 14 | 15 | unlink_path <- function(path) { 16 | if (fs::dir_exists(path)) { 17 | if (fs::is_link(path)) { 18 | fs::link_delete(path) 19 | } else { 20 | fs::dir_delete(path) 21 | } 22 | } 23 | } 24 | 25 | 26 | collapse <- function(...) { 27 | paste0(..., collapse = "\n") 28 | } 29 | 30 | package_json_version <- function(source_dir) { 31 | package_json_path <- fs::path(source_dir, "package.json") 32 | if (!fs::file_exists(package_json_path)) { 33 | cli::cli_abort( 34 | "{.field package.json} does not exist in {.path {source_dir}}" 35 | ) 36 | } 37 | 38 | package_json <- jsonlite::read_json(package_json_path) 39 | package_json$version 40 | } 41 | 42 | 43 | files_are_equal <- function(x_file_path, y_file_path) { 44 | tools::md5sum(x_file_path) == tools::md5sum(y_file_path) 45 | } 46 | 47 | drop_nulls_rec <- function(x) { 48 | if (is.list(x)) { 49 | # Recurse 50 | x <- lapply(x, drop_nulls_rec) 51 | is_null <- vapply(x, is.null, logical(1)) 52 | x[!is_null] 53 | } else { 54 | # Return as is. Let parent list handle it 55 | x 56 | } 57 | } 58 | 59 | 60 | # """Returns a function that can be used as a copy_function for shutil.copytree. 61 | # 62 | # If overwrite is True, the copy function will overwrite files that already exist. 63 | # If overwrite is False, the copy function will not overwrite files that already exist. 64 | # """ 65 | # Using base file methods in this function because `{fs}` is slow. 66 | # Perform the file copying in two stages: 67 | # 1. Mark all files to be copied 68 | # 2. Copy all files 69 | # IO operations are slow in R. It is faster to call `fs::file_copy()` with a large vector than many times with single values. 70 | create_copy_fn <- function(overwrite = FALSE) { 71 | overwrite <- isTRUE(overwrite) 72 | 73 | file_list <- list() 74 | 75 | mark_file <- function(src_file_path, dst_file_path) { 76 | if (file.exists(dst_file_path)) { 77 | if (!files_are_equal(src_file_path, dst_file_path)) { 78 | cli::cli_inform(c( 79 | x = "Source and destination copies differ for {.path {dst_file_path}}", 80 | "!" = "This is probably because your shinylive sources have been updated and differ from the copy in the exported app.", 81 | i = "You probably should remove the export directory and re-export the application." 82 | )) 83 | } 84 | if (overwrite) { 85 | # cli_alert_warning("Removing {.path {dst_file_print}}") 86 | unlink_path(dst_file_path) 87 | } else { 88 | # cli_alert("Skipping {.path {dst_file_print}}") 89 | return() 90 | } 91 | } else { 92 | # Make sure destination's parent directory exists 93 | parent_dir <- dirname(dst_file_path) 94 | if (!dir.exists(parent_dir)) { 95 | dir.create(parent_dir, recursive = TRUE) 96 | } 97 | # fs::dir_create(fs::path_dir(dst_file_path)) 98 | } 99 | 100 | file_list[[length(file_list) + 1]] <<- list( 101 | src_file_path = src_file_path, 102 | dst_file_path = dst_file_path 103 | ) 104 | # # Copy file 105 | # file.copy(src_file_path, dst_file_path) 106 | # # fs::file_copy(src_file_path, dst_file_path) 107 | } 108 | copy_files <- function() { 109 | if (length(file_list) == 0) { 110 | return() 111 | } 112 | src_file_paths <- vapply(file_list, `[[`, character(1), "src_file_path") 113 | dst_file_paths <- vapply(file_list, `[[`, character(1), "dst_file_path") 114 | # Because this is many files, `fs` is marginally faster 115 | fs::file_copy(src_file_paths, dst_file_paths) 116 | } 117 | list( 118 | mark_file = mark_file, 119 | copy_files = copy_files 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /man/quarto_ext.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/quarto_ext.R 3 | \name{quarto_ext} 4 | \alias{quarto_ext} 5 | \title{Quarto extension for shinylive} 6 | \usage{ 7 | quarto_ext( 8 | args = commandArgs(trailingOnly = TRUE), 9 | ..., 10 | pretty = is_interactive(), 11 | con = "stdin" 12 | ) 13 | } 14 | \arguments{ 15 | \item{args}{Command line arguments passed by the extension. See details for more information.} 16 | 17 | \item{...}{Ignored.} 18 | 19 | \item{pretty}{Whether to pretty print the JSON output.} 20 | 21 | \item{con}{File from which to take input. Default: \code{"stdin"}.} 22 | } 23 | \value{ 24 | Nothing. Values are printed to stdout. 25 | } 26 | \description{ 27 | Integration with https://github.com/quarto-ext/shinylive 28 | } 29 | \section{Command arguments}{ 30 | 31 | 32 | The first argument must be \code{"extension"}. This is done to match 33 | \code{py-shinylive} so that it can nest other sub-commands under the \code{extension} 34 | argument to minimize the api clutter the user can see. 35 | \subsection{CLI Interface}{ 36 | \itemize{ 37 | \item \verb{extension info} 38 | \itemize{ 39 | \item Prints information about the extension including: 40 | \itemize{ 41 | \item \code{version}: The version of the R package 42 | \item \code{assets_version}: The version of the web assets 43 | \item \code{scripts}: A list of paths scripts that are used by the extension, 44 | mainly \code{codeblock-to-json} 45 | } 46 | \item Example 47 | 48 | \if{html}{\out{
}}\preformatted{\{ 49 | "version": "0.1.0", 50 | "assets_version": "0.2.0", 51 | "scripts": \{ 52 | "codeblock-to-json": "//shinylive-0.2.0/scripts/codeblock-to-json.js" 53 | \} 54 | \} 55 | }\if{html}{\out{
}} 56 | } 57 | \item \verb{extension base-htmldeps} 58 | \itemize{ 59 | \item Prints the language agnostic quarto html dependencies as a JSON array. 60 | \itemize{ 61 | \item The first html dependency is the \code{shinylive} service workers. 62 | \item The second html dependency is the \code{shinylive} base dependencies. This 63 | dependency will contain the core \code{shinylive} asset scripts (JS files 64 | automatically sourced), stylesheets (CSS files that are automatically 65 | included), and resources (additional files that the JS and CSS files can 66 | source). 67 | } 68 | \item Example 69 | 70 | \if{html}{\out{
}}\preformatted{[ 71 | \{ 72 | "name": "shinylive-serviceworker", 73 | "version": "0.2.0", 74 | "meta": \{ "shinylive:serviceworker_dir": "." \}, 75 | "serviceworkers": [ 76 | \{ 77 | "source": "//shinylive-0.2.0/shinylive-sw.js", 78 | "destination": "/shinylive-sw.js" 79 | \} 80 | ] 81 | \}, 82 | \{ 83 | "name": "shinylive", 84 | "version": "0.2.0", 85 | "scripts": [\{ 86 | "name": "shinylive/load-shinylive-sw.js", 87 | "path": "//shinylive-0.2.0/shinylive/load-shinylive-sw.js", 88 | "attribs": \{ "type": "module" \} 89 | \}], 90 | "stylesheets": [\{ 91 | "name": "shinylive/shinylive.css", 92 | "path": "//shinylive-0.2.0/shinylive/shinylive.css" 93 | \}], 94 | "resources": [ 95 | \{ 96 | "name": "shinylive/shinylive.js", 97 | "path": "//shinylive-0.2.0/shinylive/shinylive.js" 98 | \}, 99 | ... # [ truncated ] 100 | ] 101 | \} 102 | ] 103 | }\if{html}{\out{
}} 104 | } 105 | \item \verb{extension language-resources} 106 | \itemize{ 107 | \item Prints the language-specific resource files as JSON that should be added to the quarto html dependency. 108 | \itemize{ 109 | \item For r-shinylive, this includes the webr resource files 110 | \item For py-shinylive, this includes the pyodide and pyright resource files. 111 | } 112 | \item Example 113 | 114 | \if{html}{\out{
}}\preformatted{[ 115 | \{ 116 | "name": "shinylive/webr/esbuild.d.ts", 117 | "path": "//shinylive-0.2.0/shinylive/webr/esbuild.d.ts" 118 | \}, 119 | \{ 120 | "name": "shinylive/webr/libRblas.so", 121 | "path": "//shinylive-0.2.0/shinylive/webr/libRblas.so" 122 | \}, 123 | ... # [ truncated ] 124 | ] 125 | }\if{html}{\out{
}} 126 | } 127 | \item \verb{extension app-resources} 128 | \itemize{ 129 | \item Prints app-specific resource files as JSON that should be added to the \code{"shinylive"} quarto html dependency. 130 | \item Currently, r-shinylive does not return any resource files. 131 | \item Example 132 | 133 | \if{html}{\out{
}}\preformatted{[ 134 | \{ 135 | "name": "shinylive/pyodide/anyio-3.7.0-py3-none-any.whl", 136 | "path": "//shinylive-0.2.0/shinylive/pyodide/anyio-3.7.0-py3-none-any.whl" 137 | \}, 138 | \{ 139 | "name": "shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl", 140 | "path": "//shinylive-0.2.0/shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl" 141 | \}, 142 | ... # [ truncated ] 143 | ] 144 | }\if{html}{\out{
}} 145 | } 146 | } 147 | } 148 | } 149 | 150 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/rstudio/shiny-workflows 2 | # 3 | # NOTE: This Shiny team GHA workflow is overkill for most R packages. 4 | # For most R packages it is better to use https://github.com/r-lib/actions 5 | on: 6 | push: 7 | branches: [main, rc-**] 8 | pull_request: 9 | branches: [main] 10 | schedule: 11 | - cron: "0 9 * * 1" # every monday 12 | 13 | name: Package checks 14 | 15 | jobs: 16 | website: 17 | uses: rstudio/shiny-workflows/.github/workflows/website.yaml@v1 18 | routine: 19 | uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1 20 | R-CMD-check: 21 | uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1 22 | 23 | integration: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Check out repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Set up Python - 3.12 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: "3.12" 33 | 34 | - name: Upgrade pip 35 | shell: bash 36 | run: | 37 | python -m pip install --upgrade pip 38 | 39 | - name: Install py-shinylive 40 | id: py-shinylive 41 | shell: bash 42 | run: | 43 | pip install shinylive 44 | # pip install https://github.com/posit-dev/py-shinylive/archive/split_api.zip 45 | echo "version=$(shinylive assets version)" >> "$GITHUB_OUTPUT" 46 | 47 | - name: Set up Quarto 48 | uses: quarto-dev/quarto-actions/setup@v2 49 | 50 | - name: Install quarto-ext/shinylive 51 | shell: bash 52 | run: | 53 | cd local/quarto 54 | quarto add quarto-ext/shinylive --no-prompt 55 | # Trouble installing from branch. Using url instead. 56 | # quarto add http://github.com/quarto-ext/shinylive/archive/v2_api.zip --no-prompt 57 | 58 | - name: Install R, system dependencies, and package dependencies 59 | uses: rstudio/shiny-workflows/setup-r-package@v1 60 | with: 61 | needs: quarto 62 | 63 | - name: Test shinylive quarto extension with py-shinylive assets version 64 | uses: quarto-dev/quarto-actions/render@v2 65 | env: 66 | SHINYLIVE_ASSETS_VERSION: ${{ steps.py-shinylive.outputs.version }} 67 | with: 68 | path: local/quarto/ 69 | 70 | - name: Check out 'posit-dev/shinylive' repo into './shinylive_assets' 71 | uses: actions/checkout@v4 72 | with: 73 | repository: posit-dev/shinylive 74 | path: shinylive_assets 75 | 76 | - name: Build shinylive assets 77 | shell: bash 78 | run: | 79 | cd shinylive_assets 80 | make submodules 81 | make all 82 | 83 | - name: Link shinylive assets 84 | id: r-linked-assets 85 | shell: Rscript {0} 86 | run: | 87 | shinylive_local_version <- shinylive:::package_json_version("shinylive_assets") 88 | shinylive::assets_remove(shinylive_local_version) 89 | shinylive::assets_install_copy("shinylive_assets") 90 | shinylive::assets_info() 91 | cat( 92 | "version=", shinylive_local_version, 93 | file = Sys.getenv("GITHUB_OUTPUT"), 94 | append = TRUE, 95 | sep = "" 96 | ) 97 | 98 | - name: Update lua script for debugging 99 | shell: Rscript {0} 100 | run: | 101 | shinylive_lua <- file.path( 102 | "local", "quarto", "_extensions", 103 | # (When installing from a zip url, there is no `quarto-ext` dir.) 104 | "quarto-ext", 105 | "shinylive", "shinylive.lua" 106 | ) 107 | shinylive_lua |> 108 | brio::read_file() |> 109 | sub( 110 | pattern = "-- print(\"Calling", 111 | replacement = "print(\"Calling", 112 | fixed = TRUE 113 | ) |> 114 | sub( 115 | pattern = "-- print(\"res", 116 | replacement = "-- print(\"res", 117 | fixed = TRUE 118 | ) |> 119 | brio::write_file(shinylive_lua) 120 | 121 | cat(brio::read_file(shinylive_lua),"\n") 122 | 123 | - name: Run shinylive R package tests 124 | env: 125 | TEST_ASSETS: "TRUE" 126 | SHINYLIVE_ASSETS_VERSION: ${{ steps.r-linked-assets.outputs.version }} 127 | shell: Rscript {0} 128 | run: | 129 | shinylive::assets_info() 130 | dir(shinylive:::assets_cache_dir()) 131 | dir(shinylive:::assets_dirs()) 132 | as.list(fs::file_info(shinylive:::assets_dirs())) 133 | 134 | shinylive::assets_ensure() 135 | 136 | testthat::test_local() 137 | 138 | #- name: Test shinylive quarto extension with latest shinylive assets 139 | # uses: quarto-dev/quarto-actions/render@v2 140 | # env: 141 | # # TODO: py-shinylive doesn't follow this envvar yet. If shinylive 142 | # # has a newer version, this action will fail. 143 | # SHINYLIVE_ASSETS_VERSION: ${{ steps.r-linked-assets.outputs.version }} 144 | # with: 145 | # path: local/quarto/ 146 | 147 | # TODO-barret-future; Test the output of the render using pyright / py-shiny e2e controls? 148 | -------------------------------------------------------------------------------- /tests/testthat/test-export.R: -------------------------------------------------------------------------------- 1 | expect_silent_unattended <- function(expr) { 2 | if (interactive()) { 3 | return(expr) 4 | } 5 | expect_silent(expr) 6 | } 7 | 8 | test_that("export - app.R", { 9 | maybe_skip_test() 10 | 11 | assets_ensure() 12 | 13 | # Ensure pkgcache metadata has been loaded 14 | invisible(pkgcache::meta_cache_list()) 15 | 16 | # Create a temporary output directory 17 | out_dir <- file.path(tempfile(), "out") 18 | on.exit(unlink_path(out_dir), add = TRUE) 19 | 20 | app_dir <- test_path("apps", "app-r") 21 | 22 | expect_silent_unattended({ 23 | export(app_dir, out_dir) 24 | }) 25 | 26 | asset_root_files <- c("shinylive", "shinylive-sw.js") 27 | asset_app_files <- c("app.json", "edit", "index.html") 28 | asset_edit_files <- c("index.html") 29 | 30 | expect_setequal( 31 | dir(out_dir), 32 | c(asset_root_files, asset_app_files) 33 | ) 34 | expect_setequal(dir(file.path(out_dir, "edit")), asset_edit_files) 35 | 36 | expect_silent_unattended({ 37 | export(app_dir, out_dir, subdir = "test_subdir") 38 | }) 39 | 40 | expect_setequal( 41 | dir(out_dir), 42 | c(asset_root_files, asset_app_files, "test_subdir") 43 | ) 44 | expect_setequal( 45 | dir(file.path(out_dir, "test_subdir")), 46 | asset_app_files 47 | ) 48 | expect_setequal( 49 | dir(file.path(out_dir, "test_subdir", "edit")), 50 | asset_edit_files 51 | ) 52 | }) 53 | 54 | 55 | test_that("export - server.R", { 56 | maybe_skip_test() 57 | 58 | assets_ensure() 59 | 60 | # Create a temporary directory 61 | out_dir <- file.path(tempfile(), "out") 62 | on.exit(unlink_path(out_dir)) 63 | 64 | app_dir <- test_path("apps", "server-r") 65 | 66 | # Verify global.R / ui.R / server.R app can be exported 67 | expect_silent_unattended({ 68 | export(app_dir, out_dir) 69 | }) 70 | 71 | # Verify global.R / ui.R / server.R exported files exist 72 | app_json <- jsonlite::read_json(file.path(out_dir, "app.json")) 73 | out_app_file_names <- vapply(app_json, `[[`, character(1), "name") 74 | expect_setequal( 75 | out_app_file_names, 76 | c("global.R", "ui.R", "server.R") 77 | ) 78 | }) 79 | 80 | test_that("export with template", { 81 | maybe_skip_test() 82 | skip_if(assets_version() <= "0.4.1") 83 | 84 | # For local testing until next release after 0.4.1 85 | # withr::local_envvar(list("SHINYLIVE_ASSETS_VERSION" = "0.4.1")) 86 | 87 | assets_ensure() 88 | 89 | path_export <- test_path("apps", "export_template") 90 | 91 | if (FALSE) { 92 | # Run this manually to re-initialize the export template, but you'll need to 93 | # add the template parameters tested below. 94 | path_export_src <- fs::path(shinylive:::assets_dir(), "export_template") 95 | fs::dir_copy(path_export_src, path_export, overwrite = TRUE) 96 | } 97 | 98 | # Create a temporary directory 99 | out_dir <- file.path(tempfile(), "out") 100 | on.exit(unlink_path(out_dir)) 101 | 102 | app_dir <- test_path("apps", "app-r") 103 | 104 | expect_silent_unattended({ 105 | export( 106 | app_dir, 107 | out_dir, 108 | template_dir = path_export, 109 | template_params = list( 110 | # Included in export template for > 0.4.1 111 | title = "Shinylive Test App", 112 | include_before_body = "

Shinylive Test App

", 113 | include_after_body = "", 114 | # Included in the customized export template in test suite 115 | description = "My custom export template param test app" 116 | ) 117 | ) 118 | }) 119 | 120 | index_content <- brio::read_file(fs::path(out_dir, "index.html")) 121 | expect_match( 122 | index_content, 123 | "Shinylive Test App" 124 | ) 125 | 126 | expect_match( 127 | index_content, 128 | "\\s+

Shinylive Test App

" 129 | ) 130 | 131 | expect_match( 132 | index_content, 133 | "\\s+" 134 | ) 135 | 136 | expect_match( 137 | index_content, 138 | "" 139 | ) 140 | }) 141 | 142 | test_that("export - include R package in wasm assets", { 143 | maybe_skip_test() 144 | 145 | assets_ensure() 146 | 147 | # Ensure pkgcache metadata has been loaded 148 | invisible(pkgcache::meta_cache_list()) 149 | 150 | # Create a temporary output directory 151 | out_dir <- file.path(tempfile(), "out") 152 | pkg_dir <- file.path(out_dir, "shinylive", "webr", "packages") 153 | 154 | # A package with an external dependency 155 | app_dir <- test_path("apps", "app-utf8") 156 | asset_package <- c("utf8") 157 | 158 | # No external dependencies exported 159 | expect_silent_unattended({ 160 | withr::with_envvar( 161 | list("SHINYLIVE_WASM_PACKAGES" = "FALSE"), 162 | export(app_dir, out_dir) 163 | ) 164 | }) 165 | expect_length(dir(pkg_dir), 0) 166 | unlink_path(out_dir) 167 | 168 | # Default filesize 100MB 169 | expect_silent_unattended({ 170 | export(app_dir, out_dir) 171 | }) 172 | expect_contains(dir(pkg_dir), c(asset_package)) 173 | unlink_path(out_dir) 174 | 175 | # No maximum filesize 176 | expect_silent_unattended({ 177 | export(app_dir, out_dir, max_filesize = Inf) 178 | }) 179 | expect_contains(dir(pkg_dir), c(asset_package)) 180 | unlink_path(out_dir) 181 | 182 | # Set a maximum filesize 183 | expect_error({ 184 | export(app_dir, out_dir, max_filesize = "1K") 185 | }) 186 | unlink_path(out_dir) 187 | 188 | expect_error({ 189 | withr::with_envvar( 190 | list("SHINYLIVE_DEFAULT_MAX_FILESIZE" = "1K"), 191 | export(app_dir, out_dir) 192 | ) 193 | }) 194 | unlink_path(out_dir) 195 | }) 196 | -------------------------------------------------------------------------------- /R/app_json.R: -------------------------------------------------------------------------------- 1 | # This is the same as the FileContentJson type in TypeScript. 2 | FILE_CONTENT_CLASS <- "shinylive_file_content" 3 | file_content_obj <- function(name, content, type = c("text", "binary")) { 4 | structure( 5 | list( 6 | name = name, 7 | content = content, 8 | type = match.arg(type) 9 | ), 10 | class = c(FILE_CONTENT_CLASS, "list") 11 | ) 12 | } 13 | 14 | APP_INFO_CLASS <- "shinylive_app_info" 15 | app_info_obj <- function(appdir, subdir, files) { 16 | stopifnot(inherits(files, "list")) 17 | lapply(files, function(file) { 18 | stopifnot(inherits(file, FILE_CONTENT_CLASS)) 19 | }) 20 | structure( 21 | list( 22 | appdir = appdir, 23 | subdir = subdir, 24 | files = files 25 | ), 26 | class = APP_INFO_CLASS 27 | ) 28 | } 29 | 30 | 31 | # ============================================================================= 32 | # """ 33 | # Load files for a Shiny application. 34 | # 35 | # Parameters 36 | # ---------- 37 | # appdir : str 38 | # Directory containing the application. 39 | # 40 | # destdir : str 41 | # Destination directory. This is used only to avoid adding shinylive assets when 42 | # they are in a subdir of the application. 43 | # """ 44 | read_app_files <- function( 45 | appdir, 46 | destdir 47 | ) { 48 | exclude_names <- c("__pycache__", "venv", ".venv", "rsconnect") 49 | # exclude_names_map <- setNames(rep(TRUE, length(exclude_names)), exclude_names) 50 | is_excluded <- function(name) { 51 | name %in% exclude_names 52 | } 53 | # Recursively iterate over files in app directory, and collect the files into 54 | # app_files data structure. 55 | 56 | # Returned from this function 57 | app_files <- list() 58 | # Add an app file entry from anywhere in `read_app_files` to avoid handling data from the bottom up 59 | add_file <- function(name, content, type) { 60 | app_files[[length(app_files) + 1]] <<- 61 | file_content_obj( 62 | name = name, 63 | content = content, 64 | type = type 65 | ) 66 | } 67 | 68 | inspect_dir <- function(curdur) { 69 | stopifnot(fs::is_dir(curdur)) 70 | 71 | # Check for excluded dirs 72 | curdur_basename <- basename(curdur) 73 | if (is_excluded(curdur_basename)) { 74 | return(NULL) 75 | } 76 | 77 | # If the current directory is inside of the destdir, then do not inspect 78 | if (fs::path_has_parent(curdur, destdir)) { 79 | return(NULL) 80 | } 81 | 82 | # Do not need to worry about names that start with `.` as 83 | # they are not returned by `fs::dir_walk(all = FALSE)`. 84 | # `dir()` is 10x faster than `fs::dir_ls()` 85 | cur_paths <- dir(curdur, full.names = TRUE) 86 | # Stable sort 87 | cur_paths <- sort(cur_paths, method = "radix") 88 | 89 | cur_paths_basename <- basename(cur_paths) 90 | # Move `app.R`, `ui.R`, `server.R` to first in list 91 | first_files <- c("app.R", "ui.R", "server.R") 92 | # print(list(cur_paths, has_first_files, has_first_files)) 93 | has_first_files <- first_files %in% cur_paths_basename 94 | is_first_file <- cur_paths_basename %in% first_files 95 | if (any(is_first_file)) { 96 | cur_paths <- c( 97 | # Only first files found 98 | cur_paths[is_first_file], 99 | # Other files without first files 100 | cur_paths[!is_first_file] 101 | ) 102 | } 103 | 104 | # For each file/dir in this directory... 105 | Map( 106 | cur_paths, 107 | f = function(cur_path) { 108 | if (fs::is_dir(cur_path)) { 109 | # Recurse 110 | inspect_dir(cur_path) 111 | return(NULL) 112 | } 113 | 114 | # cur_path is a file! 115 | 116 | cur_basename <- basename(cur_path) 117 | if (cur_basename == "shinylive.js") { 118 | cli::cli_warn(c( 119 | "Warning: Found {.path shinylive.js} in source directory {.path {curdur}}.", 120 | i = "Are you including a shinylive distribution in your app?" 121 | )) 122 | } 123 | 124 | # Get file content 125 | file_content <- try(brio::read_file(cur_path), silent = TRUE) 126 | if (!inherits(file_content, "try-error")) { 127 | file_type <- "text" 128 | } else { 129 | # Try reading as binary 130 | file_content <- brio::read_file_raw(cur_path) 131 | file_type <- "binary" 132 | } 133 | 134 | add_file( 135 | name = fs::path_rel(cur_path, appdir), 136 | content = file_content, 137 | type = file_type 138 | ) 139 | } 140 | ) 141 | invisible() 142 | } 143 | 144 | inspect_dir(appdir) 145 | 146 | app_files 147 | } 148 | 149 | 150 | # """ 151 | # Write index.html, edit/index.html, and app.json for an application in the destdir. 152 | # """ 153 | write_app_json <- function( 154 | app_info, 155 | destdir, 156 | template_dir, 157 | template_params = list(), 158 | quiet = getOption("shinylive.quiet", FALSE) 159 | ) { 160 | local_quiet(quiet) 161 | 162 | stopifnot(inherits(app_info, APP_INFO_CLASS)) 163 | # stopifnot(fs::dir_exists(destdir)) 164 | stopifnot(fs::dir_exists(template_dir)) 165 | 166 | app_destdir <- fs::path(destdir, app_info$subdir) 167 | 168 | # For a subdir like a/b/c, this will be ../../../ 169 | subdir_inverse <- paste0( 170 | rep("..", length(fs::path_split(app_info$subdir)[[1]])), 171 | collapse = "/" 172 | ) 173 | if (subdir_inverse != "") { 174 | # Add trailing slash 175 | subdir_inverse <- paste0(subdir_inverse, "/") 176 | } 177 | 178 | # Then iterate over the HTML files in the template directory and interpolate 179 | # the template parameters. 180 | template_files <- fs::dir_ls(template_dir, recurse = TRUE, type = "file") 181 | 182 | template_params <- rlang::dots_list( 183 | # Forced parameters 184 | REL_PATH = subdir_inverse, 185 | APP_ENGINE = "r", 186 | # User parameters 187 | !!!template_params, 188 | # Default parameters 189 | title = "Shiny App", 190 | .homonyms = "first" 191 | ) 192 | 193 | for (template_file in template_files) { 194 | dest_file <- fs::path( 195 | app_destdir, 196 | fs::path_rel(template_file, template_dir) 197 | ) 198 | fs::dir_create(fs::path_dir(dest_file)) 199 | 200 | if (fs::path_ext(template_file) == "html") { 201 | file_content <- whisker::whisker.render( 202 | template = brio::read_file(template_file), 203 | data = template_params 204 | ) 205 | brio::write_file(file_content, dest_file) 206 | } else { 207 | fs::file_copy(template_file, dest_file) 208 | } 209 | } 210 | 211 | app_json_output_file <- fs::path(app_destdir, "app.json") 212 | 213 | cli_progress_step("Writing {.path {app_json_output_file}}") 214 | jsonlite::write_json( 215 | app_info$files, 216 | path = app_json_output_file, 217 | auto_unbox = TRUE, 218 | pretty = FALSE 219 | ) 220 | cli_progress_done() 221 | cli_alert_info( 222 | "Wrote {.path {app_json_output_file}} ({fs::file_info(app_json_output_file)$size[1]} bytes)" 223 | ) 224 | 225 | invisible(app_json_output_file) 226 | } 227 | -------------------------------------------------------------------------------- /R/export.R: -------------------------------------------------------------------------------- 1 | #' Export a Shiny app to a directory 2 | #' 3 | #' This function exports a Shiny app to a directory, which can then be served 4 | #' using `httpuv`. 5 | #' 6 | #' @param appdir Directory containing the application. 7 | #' @param destdir Destination directory. 8 | #' @param subdir Subdirectory of `destdir` to write the app to. 9 | #' @param quiet Suppress console output during export. Follows the global 10 | #' `shinylive.quiet` option or defaults to `FALSE` in interactive sessions if 11 | #' not set. 12 | #' @param verbose Deprecated, please use `quiet` instead. 13 | #' @param wasm_packages Download and include binary WebAssembly packages as part 14 | #' of the output app's static assets. Logical, defaults to `TRUE`. The default 15 | #' value can be changed by setting the environment variable 16 | #' `SHINYLIVE_WASM_PACKAGES` to `TRUE` or `1` to enable, `FALSE` or `0` to 17 | #' disable. 18 | #' @param package_cache Cache downloaded binary WebAssembly packages. Defaults 19 | #' to `TRUE`. 20 | #' @param max_filesize Maximum file size for bundling of WebAssembly package 21 | #' assets. Parsed by [fs::fs_bytes()]. Defaults to `"100M"`. The default 22 | #' value can be changed by setting the environment variable 23 | #' `SHINYLIVE_DEFAULT_MAX_FILESIZE`. Set to `Inf`, `NA` or `-1` to disable. 24 | #' @param assets_version The version of the Shinylive assets to use in the 25 | #' exported app. Defaults to [assets_version()]. Note, not all custom assets 26 | #' versions may work with this release of \pkg{shinylive}. Please visit the 27 | #' [shinylive asset releases](https://github.com/posit-dev/shinylive/releases) 28 | #' website to learn more information about the available `assets_version` 29 | #' values. 30 | #' @param template_dir Path to a custom template directory to use when exporting 31 | #' the shinylive app. The template can be copied from the shinylive assets 32 | #' using: `fs::path(shinylive:::assets_dir(), "export_template")`. 33 | #' @param template_params A list of parameters to pass to the template. The 34 | #' supported parameters depends on the template being used. Custom templates 35 | #' may support additional parameters (see `template_dir` for instructions on 36 | #' creating a custom template or to find the current shinylive assets' 37 | #' templates). 38 | #' 39 | #' With shinylive assets > 0.4.1, the default export template supports the 40 | #' following parameters: 41 | #' 42 | #' 1. `title`: The title of the app. Defaults to `"Shiny app"`. 43 | #' 2. `include_in_head`, `include_before_body`, `include_after_body`: Raw 44 | #' HTML to be included in the ``, just after the opening ``, 45 | #' or just before the closing `` tag, respectively. 46 | #' @param ... Ignored 47 | #' @export 48 | #' @return Nothing. The app is exported to `destdir`. Instructions for serving 49 | #' the directory are printed to stdout. 50 | #' @examplesIf rlang::is_interactive() 51 | #' app_dir <- system.file("examples", "01_hello", package = "shiny") 52 | #' out_dir <- tempfile("shinylive-export") 53 | #' 54 | #' # Export the app to a directory 55 | #' export(app_dir, out_dir) 56 | #' 57 | #' # Serve the exported directory 58 | #' if (require(httpuv)) { 59 | #' httpuv::runStaticServer(out_dir) 60 | #' } 61 | export <- function( 62 | appdir, 63 | destdir, 64 | ..., 65 | subdir = "", 66 | quiet = getOption("shinylive.quiet", !is_interactive()), 67 | wasm_packages = NULL, 68 | package_cache = TRUE, 69 | max_filesize = NULL, 70 | assets_version = NULL, 71 | template_dir = NULL, 72 | template_params = list(), 73 | verbose = NULL 74 | ) { 75 | if (!is.null(verbose)) { 76 | rlang::warn( 77 | "The {.var verbose} argument is deprecated. Use {.var quiet} instead." 78 | ) 79 | if (missing(quiet)) { 80 | quiet <- !verbose 81 | } 82 | } 83 | 84 | local_quiet(quiet) 85 | cli_alert_info("Exporting Shiny app from: {.path {appdir}}") 86 | cli_alert("Destination: {.path {destdir}}") 87 | 88 | if (is.null(assets_version)) { 89 | assets_version <- assets_version() 90 | } 91 | 92 | wasm_packages <- wasm_packages %||% sys_env_wasm_packages() 93 | 94 | if (!fs::is_dir(appdir)) { 95 | cli::cli_abort( 96 | "{.var appdir} must be a directory, but was provided {.path {appdir}}." 97 | ) 98 | } 99 | if ( 100 | !(fs::file_exists(fs::path(appdir, "app.R")) || 101 | fs::file_exists(fs::path(appdir, "server.R"))) 102 | ) { 103 | cli::cli_abort( 104 | "Directory {.path {appdir}} does not contain an app.R or server.R file." 105 | ) 106 | } 107 | 108 | if (fs::is_absolute_path(subdir)) { 109 | cli::cli_abort( 110 | "{.var subdir} was supplied an absolute path ({.path {subdir}}), but only relative paths are allowed." 111 | ) 112 | } 113 | 114 | if (!fs::dir_exists(destdir)) { 115 | fs::dir_create(destdir) 116 | } 117 | 118 | cp_funcs <- create_copy_fn(overwrite = FALSE) 119 | mark_file <- cp_funcs$mark_file 120 | copy_files <- cp_funcs$copy_files 121 | 122 | assets_path <- assets_dir(version = assets_version) 123 | 124 | # ========================================================================= 125 | # Copy the base dependencies for shinylive/ distribution. This does not 126 | # include the R package files. 127 | # ========================================================================= 128 | 129 | # When exporting, we know it is only an R app. So remove python support 130 | base_files <- c( 131 | shinylive_common_files("base", version = assets_version), 132 | shinylive_common_files("r", version = assets_version) 133 | ) 134 | 135 | if (!is_quiet()) { 136 | cli::cli_progress_bar( 137 | "Copying base Shinylive files", 138 | total = length(base_files), 139 | type = "tasks" 140 | ) 141 | } 142 | 143 | for (base_file in base_files) { 144 | src_path <- file.path(assets_path, base_file) 145 | dest_path <- file.path(destdir, base_file) 146 | 147 | if (!is_quiet()) { 148 | cli::cli_progress_update() 149 | } 150 | mark_file(src_path, dest_path) 151 | } 152 | 153 | # lapply(base_files, function(base_file) { 154 | # src_path <- fs::path(assets_path, base_file) 155 | # dest_path <- fs::path(destdir, base_file) 156 | # if (verbose) { 157 | # p$tick() 158 | # } 159 | 160 | # # Add file to copy list 161 | # copy_fn(src_path, dest_path) 162 | # }) 163 | # Copy all files in one call 164 | copy_files() 165 | cli_progress_done() 166 | 167 | # ========================================================================= 168 | # Load each app's contents into a list[FileContentJson] 169 | # ========================================================================= 170 | app_info <- app_info_obj( 171 | appdir, 172 | subdir, 173 | read_app_files(appdir, destdir) 174 | ) 175 | 176 | # # ========================================================================= 177 | # # Copy dependencies from shinylive/pyodide/ 178 | # # ========================================================================= 179 | # if full_shinylive: 180 | # package_files = _utils.listdir_recursive(assets_path / "shinylive" / "pyodide") 181 | # # Some of the files in this dir are base files; don't copy them. 182 | # package_files = [ 183 | # file 184 | # for file in package_files 185 | # if os.path.join("shinylive", "pyodide", file) not in base_files 186 | # ] 187 | 188 | # else: 189 | # deps = _deps.base_package_deps() + _deps.find_package_deps(app_info["files"]) 190 | 191 | # package_files: list[str] = [dep["file_name"] for dep in deps] 192 | 193 | # print( 194 | # f"Copying imported packages from {assets_path}/shinylive/pyodide/ to {destdir}/shinylive/pyodide/", 195 | # file=sys.stderr, 196 | # ) 197 | # verbose_print(" ", ", ".join(package_files)) 198 | 199 | # for filename in package_files: 200 | # src_path = assets_path / "shinylive" / "pyodide" / filename 201 | # dest_path = destdir / "shinylive" / "pyodide" / filename 202 | # if not dest_path.parent.exists(): 203 | # os.makedirs(dest_path.parent) 204 | 205 | # copy_fn(src_path, dest_path) 206 | 207 | # ========================================================================= 208 | # Copy app package dependencies as Wasm binaries 209 | # ========================================================================= 210 | if (wasm_packages && wasm_packages_able(assets_version)) { 211 | download_wasm_packages(appdir, destdir, package_cache, max_filesize) 212 | } 213 | 214 | # ========================================================================= 215 | # For each app, write the index.html, edit/index.html, and app.json in 216 | # destdir/subdir. 217 | # ========================================================================= 218 | write_app_json( 219 | app_info, 220 | destdir, 221 | template_dir = template_dir %||% fs::path(assets_path, "export_template"), 222 | quiet = quiet, 223 | template_params = template_params 224 | ) 225 | 226 | # Escape backslashes in destdir because Windows 227 | destdir_esc <- gsub("\\\\", "\\\\\\\\", destdir) 228 | 229 | cli_alert_success("Shinylive app export complete.") 230 | cli_alert_info("Run the following in an R session to serve the app:") 231 | cli_text('{.run httpuv::runStaticServer("{destdir_esc}")}') 232 | 233 | invisible(destdir) 234 | } 235 | 236 | wasm_packages_able <- function(assets_version) { 237 | if (assets_version <= package_version("0.7.0")) { 238 | cli::cli_warn(c( 239 | "Can't bundle WebAssembly R packages for legacy Shinylive assets version: {assets_version}.", 240 | "i" = "Use Shinylive assets version 0.8.0 or later to bundle WebAssembly R package binaries." 241 | )) 242 | FALSE 243 | } else { 244 | TRUE 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /R/deps.R: -------------------------------------------------------------------------------- 1 | HTML_DEP_ITEM_CLASS <- "shinylive_html_dep" 2 | html_dep_obj <- function( 3 | ..., 4 | name, 5 | path, 6 | attribs = NULL 7 | ) { 8 | stopifnot(length(list(...)) == 0) 9 | assert_nzchar_string(name) 10 | assert_nzchar_string(path) 11 | is.null(attribs) || assert_list(attribs) 12 | ret <- list( 13 | name = name, 14 | path = path 15 | ) 16 | if (!is.null(attribs)) { 17 | ret$attribs <- attribs 18 | } 19 | structure( 20 | ret, 21 | class = c(HTML_DEP_ITEM_CLASS, "list") 22 | ) 23 | } 24 | 25 | HTML_DEP_SERVICEWORKER_CLASS <- "shinylive_html_dep_serviceworker" 26 | html_dep_serviceworker_obj <- function( 27 | ..., 28 | source, 29 | destination 30 | ) { 31 | stopifnot(length(list(...)) == 0) 32 | assert_nzchar_string(source) 33 | assert_nzchar_string(destination) 34 | structure( 35 | list( 36 | source = source, 37 | destination = destination 38 | ), 39 | class = c(HTML_DEP_SERVICEWORKER_CLASS, "list") 40 | ) 41 | } 42 | 43 | QUARTO_HTML_DEPENDENCY_CLASS <- "shinylive_quarto_html_dependency" 44 | quarto_html_dependency_obj <- function( 45 | ..., 46 | name, 47 | version = NULL, 48 | scripts = NULL, 49 | stylesheets = NULL, 50 | resources = NULL, 51 | meta = NULL, 52 | head = NULL, 53 | serviceworkers = NULL 54 | ) { 55 | stopifnot(length(list(...)) == 0) 56 | assert_nzchar_string(name) 57 | is.null(version) || assert_nzchar_string(version) 58 | is.null(scripts) || assert_list_items(scripts, HTML_DEP_ITEM_CLASS) 59 | is.null(stylesheets) || assert_list_items(stylesheets, HTML_DEP_ITEM_CLASS) 60 | is.null(resources) || assert_list_items(resources, HTML_DEP_ITEM_CLASS) 61 | is.null(meta) || assert_list(meta) 62 | is.null(head) || assert_nzchar_string(head) 63 | is.null(serviceworkers) || 64 | assert_list_items(serviceworkers, HTML_DEP_SERVICEWORKER_CLASS) 65 | 66 | structure( 67 | list( 68 | name = name, 69 | version = version, 70 | scripts = scripts, 71 | stylesheets = stylesheets, 72 | resources = resources, 73 | meta = meta, 74 | head = head, 75 | serviceworkers = serviceworkers 76 | ), 77 | class = c(QUARTO_HTML_DEPENDENCY_CLASS, "list") 78 | ) 79 | } 80 | 81 | shinylive_base_deps_htmldep <- function( 82 | sw_dir = NULL, 83 | version = assets_version() 84 | ) { 85 | list( 86 | serviceworker_dep(sw_dir, version = version), 87 | shinylive_common_dep_htmldep("base", version = version) 88 | ) 89 | } 90 | shinylive_r_resources <- function(version = assets_version()) { 91 | shinylive_common_dep_htmldep("r", version = version)$resources 92 | } 93 | # Not used in practice! 94 | shinylive_python_resources <- function( 95 | sw_dir = NULL, 96 | version = assets_version() 97 | ) { 98 | shinylive_common_dep_htmldep("python", version = version)$resources 99 | } 100 | 101 | 102 | serviceworker_dep <- function(sw_dir, version = assets_version()) { 103 | quarto_html_dependency_obj( 104 | name = "shinylive-serviceworker", 105 | version = version, 106 | serviceworkers = list( 107 | html_dep_serviceworker_obj( 108 | source = file.path(assets_dir(version = version), "shinylive-sw.js"), 109 | destination = "/shinylive-sw.js" 110 | ) 111 | ), 112 | meta = if (!is.null(sw_dir)) { 113 | # Add meta tag to tell load-shinylive-sw.js where to find 114 | # shinylive-sw.js. 115 | list("shinylive:serviceworker_dir" = sw_dir) 116 | } else { 117 | NULL 118 | } 119 | ) 120 | } 121 | 122 | 123 | # """ 124 | # Return an HTML dependency object consisting of files that are base 125 | # dependencies; in other words, the files that are always included in a 126 | # Shinylive deployment. 127 | # """ 128 | shinylive_common_dep_htmldep <- function( 129 | dep_type = c("base", "python", "r"), 130 | version = assets_version() 131 | ) { 132 | assets_path <- assets_dir(version = version) 133 | # In quarto ext, keep support for python engine 134 | rel_common_files <- shinylive_common_files(dep_type = dep_type) 135 | abs_common_files <- file.path(assets_path, rel_common_files) 136 | 137 | # `NULL` values can be inserted into; 138 | # Ex: `a <- NULL; a[[1]] <- 4; stopifnot(identical(a, list(4)))` 139 | scripts <- NULL 140 | stylesheets <- NULL 141 | resources <- NULL 142 | 143 | switch( 144 | dep_type, 145 | "python" = , 146 | "r" = { 147 | # Language specific files are all resources 148 | # For speed / simplicity, create deps directly 149 | resources <- Map( 150 | USE.NAMES = FALSE, 151 | rel_common_files, 152 | abs_common_files, 153 | f = function(rel_common_file, abs_common_file) { 154 | html_dep_obj( 155 | name = rel_common_file, 156 | path = abs_common_file 157 | ) 158 | } 159 | ) 160 | }, 161 | "base" = { 162 | # Placeholder for load-shinylive-sw.js; (Existance is validated later) 163 | load_shinylive_dep <- NULL 164 | # Placeholder for run-python-blocks.js; Appended to end of scripts 165 | run_python_blocks_dep <- NULL 166 | 167 | Map( 168 | rel_common_files, 169 | abs_common_files, 170 | basename(rel_common_files), 171 | f = function(rel_common_file, abs_common_file, common_file_basename) { 172 | switch( 173 | common_file_basename, 174 | "run-python-blocks.js" = { 175 | run_python_blocks_dep <<- 176 | html_dep_obj( 177 | name = rel_common_file, 178 | path = abs_common_file, 179 | attribs = list(type = "module") 180 | ) 181 | }, 182 | "load-shinylive-sw.js" = { 183 | load_shinylive_dep <<- 184 | html_dep_obj( 185 | name = rel_common_file, 186 | path = abs_common_file, 187 | attribs = list(type = "module") 188 | ) 189 | }, 190 | "shinylive.css" = { 191 | stylesheets[[length(stylesheets) + 1]] <<- 192 | html_dep_obj( 193 | name = rel_common_file, 194 | path = abs_common_file 195 | ) 196 | }, 197 | { 198 | # Resource file 199 | resources[[length(resources) + 1]] <<- 200 | html_dep_obj( 201 | name = rel_common_file, 202 | path = abs_common_file 203 | ) 204 | } 205 | ) 206 | # Do not return anything 207 | NULL 208 | } 209 | ) 210 | 211 | # Put load-shinylive-sw.js in the scripts first 212 | if (is.null(load_shinylive_dep)) { 213 | cli::cli_abort("{.path load-shinylive-sw.js} not found in assets") 214 | } 215 | scripts <- c(list(load_shinylive_dep), scripts) 216 | 217 | # Append run_python_blocks_dep if it exists 218 | if (!is.null(run_python_blocks_dep)) { 219 | scripts[[length(scripts) + 1]] <- run_python_blocks_dep 220 | } 221 | }, 222 | { 223 | cli::cli_abort("Unknown {.var dep_type}: {.val dep_type}") 224 | } 225 | ) 226 | 227 | # # Add base python packages as resources 228 | # python: `resources.extend(base_package_deps_htmldepitems())` 229 | 230 | quarto_html_dependency_obj( 231 | # MUST be called `"shinylive"` to match quarto ext name 232 | name = "shinylive", 233 | version = assets_version(), 234 | scripts = scripts, 235 | stylesheets = stylesheets, 236 | resources = resources 237 | ) 238 | } 239 | 240 | 241 | # """ 242 | # Return a list of files that are base dependencies; in other words, the files 243 | # that are always included in a Shinylive deployment. 244 | # """ 245 | shinylive_common_files <- function( 246 | dep_type = c("base", "python", "r"), 247 | version = assets_version() 248 | ) { 249 | dep_type <- match.arg(dep_type) 250 | assets_ensure(version = version) 251 | 252 | assets_folder <- assets_dir(version = version) 253 | # # `dir()` is 10x faster than `fs::dir_ls()` 254 | # common_files <- dir(assets_folder, recursive = TRUE) 255 | 256 | asset_files_in_folder <- function(assets_sub_path, recurse) { 257 | folder <- 258 | if (is.null(assets_sub_path)) { 259 | assets_folder 260 | } else { 261 | file.path(assets_folder, assets_sub_path) 262 | } 263 | files <- dir(folder, recursive = recurse, full.names = FALSE) 264 | rel_files <- 265 | if (is.null(assets_sub_path)) { 266 | files 267 | } else { 268 | file.path(assets_sub_path, files) 269 | } 270 | if (recurse) { 271 | # Does not contain dirs by definition 272 | # Return as is 273 | rel_files 274 | } else { 275 | # Remove directories 276 | rel_files[!file.info(file.path(folder, files))$isdir] 277 | } 278 | } 279 | 280 | common_files <- 281 | switch( 282 | dep_type, 283 | "base" = { 284 | # Do copy any "top-level" python files as they are minimal 285 | c( 286 | # Do not include `./scripts` or `./export_template` in base deps 287 | asset_files_in_folder(NULL, recurse = FALSE), 288 | # Do not include `./shinylive/examples.json` in base deps 289 | setdiff( 290 | asset_files_in_folder("shinylive", recurse = FALSE), 291 | "shinylive/examples.json" 292 | ) 293 | ) 294 | }, 295 | "r" = { 296 | c( 297 | asset_files_in_folder(file.path("shinylive", "webr"), recurse = TRUE) 298 | ) 299 | }, 300 | "python" = { 301 | c( 302 | asset_files_in_folder( 303 | file.path("shinylive", "pyodide"), 304 | recurse = TRUE 305 | ), 306 | asset_files_in_folder( 307 | file.path("shinylive", "pyright"), 308 | recurse = TRUE 309 | ) 310 | ) 311 | }, 312 | { 313 | cli::cli_abort("Unknown {.var dep_type}: {.val dep_type}") 314 | } 315 | ) 316 | 317 | # Return relative path to the assets in `assets_dir()` 318 | common_files 319 | } 320 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # shinylive 3 | 4 | 5 | [![R-CMD-check](https://github.com/posit-dev/r-shinylive/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/posit-dev/r-shinylive/actions/workflows/R-CMD-check.yaml) 6 | [![CRAN status](https://www.r-pkg.org/badges/version/shinylive)](https://CRAN.R-project.org/package=shinylive) 7 | 8 | 9 | 10 | 11 | The goal of the `{shinylive}` R package is to help you create Shinylive applications from your [Shiny for R](https://shiny.posit.co) applications. 12 | Shinylive is a new way to run Shiny entirely in the browser, without any need for a hosted server, using WebAssembly via the [webR](https://docs.r-wasm.org/webr/latest/) project. 13 | 14 | ## About Shinylive 15 | 16 | The Shinylive project consists of four interdependent components that work together in several different contexts. 17 | 18 | 1. Shinylive ([posit-dev/shinylive](https://github.com/posit-dev/shinylive)) is web assets library that runs Shiny applications in the browser. You can try it out online at [shinylive.io/r](https://shinylive.io/r) or [shinylive.io/py](https://shinylive.io/py). For a more in-depth exploration of the Shinylive web assets, please check out https://www.tidyverse.org/blog/2024/10/shinylive-0-8-0/. 19 | 20 | 2. The `{shinylive}` R package ([posit-dev/r-shinylive](https://github.com/posit-dev/r-shinylive)) helps you export your Shiny applications from local files to a directory that can be hosted on a static web server. 21 | 22 | The R package also downloads the Shinylive web assets mentioned above and manages them in a local cache. These assets are included in the exported Shinylive applications and are used to run your Shiny app in the browser. 23 | 24 | 3. The [shinylive Python package](https://shiny.posit.co/py/docs/shinylive.html) ([posit-dev/py-shinylive](https://github.com/posit-dev/py-shinylive)) serves the same role as `{shinylive}` but for Shiny for Python applications. 25 | 26 | 4. The [shinylive Quarto extension](https://quarto-ext.github.io/shinylive/) ([quarto-ext/shinylive](https://github.com/quarto-ext/shinylive)) lets you write Shiny applications in [Quarto web documents and slides](https://quarto.org) and uses the R or Python package (or both) to translate `shinylive-r` or `shinylive-py` code blocks into Shinylive applications. 27 | 28 | 29 | ## Installation 30 | 31 | You can install the released version of shinylive from CRAN via: 32 | 33 | ``` r 34 | install.packages("shinylive") 35 | ``` 36 | 37 | You can install the development version of shinylive from GitHub via: 38 | 39 | ``` r 40 | # install.packages("pak") 41 | pak::pak("posit-dev/r-shinylive") 42 | ``` 43 | 44 | ## Usage 45 | 46 | To get started, we'll create a basic shiny application in a new directory `myapp/`. If you have an existing Shiny application, you can skip this step and replace `"myapp"` with the path to your existing app. 47 | 48 | ``` r 49 | # Copy "Hello World" from `{shiny}` 50 | system.file("examples", "01_hello", package="shiny") |> 51 | fs::dir_copy("myapp", overwrite = TRUE) 52 | ``` 53 | 54 | Once you have a Shiny application in `myapp/` and would like turn it into a Shinylive app in `site/`: 55 | 56 | ``` r 57 | shinylive::export("myapp", "site") 58 | ``` 59 | 60 | Then you can preview the application by running a web server and visiting it in a browser: 61 | 62 | ``` r 63 | httpuv::runStaticServer("site/") 64 | ``` 65 | 66 | At this point, you can deploy the `site/` directory to any static web hosting service. 67 | 68 | 69 | ### Multiple applications 70 | 71 | If you have multiple applications that you want to put on the same site, you can export them to subdirectories of the site, so that they can all share the same Shinylive assets. You can do this with the `--subdir` option: 72 | 73 | ``` r 74 | shinylive::export("myapp1", "site", subdir = "app1") 75 | shinylive::export("myapp2", "site", subdir = "app2") 76 | ``` 77 | 78 | ### GitHub Pages 79 | 80 | `posit-dev/r-shiny` has a workflow to automatically deploy your Shiny app from the root directory in your GitHub repository to its GitHub Pages. You can add this workflow to your repo with help from [usethis](https://usethis.r-lib.org/). 81 | 82 | ```r 83 | usethis::use_github_action(url="https://github.com/posit-dev/r-shinylive/blob/actions-v1/examples/deploy-app.yaml") 84 | ``` 85 | 86 | For more information, see the [examples folder](https://github.com/posit-dev/r-shinylive/tree/actions-v1/examples). 87 | 88 | 89 | ## R package availability 90 | 91 | The `{shinylive}` web assets will statically inspect which packages are being used in your app. 92 | 93 | If your app includes a package that is not automatically discovered, you can add an impossible-to-reach code within your Shiny application that has a library call to that R package. For example: 94 | 95 | ```r 96 | if (FALSE) { 97 | library(HIDDEN_CRAN_PKG) 98 | } 99 | ``` 100 | 101 | If you'd rather handle it manually, call `webr::install("CRAN_PKG")` in your Shiny application before calling `library(CRAN_PKG)` or `require("CRAN_PKG")`. 102 | 103 | If an R package has trouble loading, visit https://repo.r-wasm.org/ to see if it is able to be installed as a precompiled WebAssembly binary. 104 | 105 | > [Note from `{webr}`](https://docs.r-wasm.org/webr/latest/packages.html#building-r-packages-for-webr):
106 | > It is not possible to install packages from source in webR. This is not likely to change in the near future, as such a process would require an entire C and Fortran compiler toolchain to run inside the browser. For the moment, providing pre-compiled WebAssembly binaries is the only supported way to install R packages in webR. 107 | 108 | 109 | ## Shinylive asset management 110 | 111 | Each version of the Shinylive R package is associated with a particular version of the Shinylive web assets. ([See the releases here](https://github.com/posit-dev/shinylive/releases).) 112 | 113 | To see which version of this R package you have, and which version of the web assets it is associated with, simply run `shinylive::assets_info()` in your R session. It will also show which asset versions you have already installed locally: 114 | 115 | ``` r 116 | shinylive::assets_info() 117 | #> shinylive R package version: 0.1.0 118 | #> shinylive web assets version: 0.2.1 119 | #> 120 | #> Local cached shinylive asset dir: 121 | #> /Users/username/Library/Caches/shinylive 122 | #> 123 | #> Installed assets: 124 | #> /Users/username/Library/Caches/shinylive/0.2.1 125 | #> /Users/username/Library/Caches/shinylive/0.2.0 126 | ``` 127 | 128 | The web assets will be downloaded and cached the first time you run `shinylive::export()`. Or, you can run `shinylive::assets_download()` to fetch them manually. 129 | 130 | ``` r 131 | shinylive::assets_download("0.1.5") 132 | #> Downloading shinylive v0.1.5... 133 | #> Unzipping to /Users/username/Library/Caches/shinylive/ 134 | ``` 135 | 136 | You can remove old versions with `shinylive::assets_cleanup()`. This will remove all versions except the one that the Python package wants to use: 137 | 138 | ``` r 139 | shinylive::assets_cleanup() 140 | #> Keeping version 0.2.1 141 | #> Removing /Users/username/Library/Caches/shinylive/0.2.0 142 | #> Removing /Users/username/Library/Caches/shinylive/0.1.5 143 | ``` 144 | 145 | To remove a specific version, use `shinylive::assets_remove()`: 146 | 147 | ``` r 148 | shinylive::assets_remove("0.2.1") 149 | #> Removing /Users/username/Library/Caches/shinylive/0.2.1 150 | ``` 151 | 152 | ## Known limitations 153 | 154 | * [Note from `{webr}`](https://docs.r-wasm.org/webr/latest/packages.html#building-r-packages-for-webr): 155 | * > It is not possible to install packages from source in webR. This is not likely to change in the near future, as such a process would require an entire C and Fortran compiler toolchain to run inside the browser. For the moment, providing pre-compiled WebAssembly binaries is the only supported way to install R packages in webR. 156 | 157 | 158 | ## Development 159 | 160 | ### Setup - shinylive assets 161 | 162 | Works with latest GitHub version of [`posit-dev/shinylive`](https://github.com/posit-dev/shinylive/) (>= v`0.2.1`). 163 | 164 | Before linking the shinylive assets to the asset cache folder, you must first build the shiny live assets: 165 | 166 | ```bash 167 | ## In your shinylive assets repo 168 | # cd PATH/TO/posit-dev/shinylive 169 | 170 | # Generate the shiny live assets 171 | make submodules all 172 | ``` 173 | 174 | Then link the assets (using the `{shinylive}` R package) to the asset cache folder so that changes to the assets are automatically in your cached shinylive assets: 175 | 176 | ```r 177 | # Link to your local shinylive repo 178 | shinylive::assets_install_link("PATH/TO/posit-dev/shinylive") 179 | ``` 180 | 181 | ### Setup - quarto 182 | 183 | In your quarto project, call the following lines in the terminal to install the updated shinylive quarto extension: 184 | 185 | ```bash 186 | # Go to the quarto project directory 187 | cd local/quarto 188 | 189 | # Install the updated shinylive quarto extension 190 | quarto add quarto-ext/shinylive 191 | ``` 192 | 193 | By default, the extension will used the installed `{shinylive}` R package. To use the local `{shinylive}` R package, run the following in your R session to update the quarto extension locally: 194 | 195 | ```R 196 | if (!require("pkgload")) install.packages("pkgload") 197 | 198 | shinylive_lua <- file.path("local", "quarto", "_extensions", "quarto-ext", "shinylive", "shinylive.lua") 199 | shinylive_lua |> 200 | brio::read_file() |> 201 | sub( 202 | pattern = "shinylive::quarto_ext()", 203 | replacement = "pkgload::load_all('../../', quiet = TRUE); shinylive::quarto_ext()", 204 | fixed = TRUE 205 | ) |> 206 | brio::write_file(shinylive_lua) 207 | ``` 208 | 209 | ### Execute - `export()` 210 | 211 | Export a local app to a directory and run it: 212 | 213 | ```r 214 | library(httpuv) # >= 1.6.12 215 | pkgload::load_all() 216 | 217 | # Delete prior 218 | unlink("local/shiny-apps-out/", recursive = TRUE) 219 | export("local/shiny-apps/simple-r", "local/shiny-apps-out") 220 | 221 | # Host the local directory 222 | httpuv::runStaticServer("local/shiny-apps-out/") 223 | ``` 224 | -------------------------------------------------------------------------------- /R/quarto_ext.R: -------------------------------------------------------------------------------- 1 | #' Quarto extension for shinylive 2 | #' 3 | #' Integration with https://github.com/quarto-ext/shinylive 4 | #' 5 | #' @param args Command line arguments passed by the extension. See details for more information. 6 | #' @param ... Ignored. 7 | #' @param pretty Whether to pretty print the JSON output. 8 | #' @param con File from which to take input. Default: `"stdin"`. 9 | #' @return Nothing. Values are printed to stdout. 10 | #' @section Command arguments: 11 | #' 12 | #' The first argument must be `"extension"`. This is done to match 13 | #' `py-shinylive` so that it can nest other sub-commands under the `extension` 14 | #' argument to minimize the api clutter the user can see. 15 | #' 16 | #' ### CLI Interface 17 | #' * `extension info` 18 | #' * Prints information about the extension including: 19 | #' * `version`: The version of the R package 20 | #' * `assets_version`: The version of the web assets 21 | #' * `scripts`: A list of paths scripts that are used by the extension, 22 | #' mainly `codeblock-to-json` 23 | #' * Example 24 | #' ``` 25 | #' { 26 | #' "version": "0.1.0", 27 | #' "assets_version": "0.2.0", 28 | #' "scripts": { 29 | #' "codeblock-to-json": "//shinylive-0.2.0/scripts/codeblock-to-json.js" 30 | #' } 31 | #' } 32 | #' ``` 33 | #' * `extension base-htmldeps` 34 | #' * Prints the language agnostic quarto html dependencies as a JSON array. 35 | #' * The first html dependency is the `shinylive` service workers. 36 | #' * The second html dependency is the `shinylive` base dependencies. This 37 | #' dependency will contain the core `shinylive` asset scripts (JS files 38 | #' automatically sourced), stylesheets (CSS files that are automatically 39 | #' included), and resources (additional files that the JS and CSS files can 40 | #' source). 41 | #' * Example 42 | #' ``` 43 | #' [ 44 | #' { 45 | #' "name": "shinylive-serviceworker", 46 | #' "version": "0.2.0", 47 | #' "meta": { "shinylive:serviceworker_dir": "." }, 48 | #' "serviceworkers": [ 49 | #' { 50 | #' "source": "//shinylive-0.2.0/shinylive-sw.js", 51 | #' "destination": "/shinylive-sw.js" 52 | #' } 53 | #' ] 54 | #' }, 55 | #' { 56 | #' "name": "shinylive", 57 | #' "version": "0.2.0", 58 | #' "scripts": [{ 59 | #' "name": "shinylive/load-shinylive-sw.js", 60 | #' "path": "//shinylive-0.2.0/shinylive/load-shinylive-sw.js", 61 | #' "attribs": { "type": "module" } 62 | #' }], 63 | #' "stylesheets": [{ 64 | #' "name": "shinylive/shinylive.css", 65 | #' "path": "//shinylive-0.2.0/shinylive/shinylive.css" 66 | #' }], 67 | #' "resources": [ 68 | #' { 69 | #' "name": "shinylive/shinylive.js", 70 | #' "path": "//shinylive-0.2.0/shinylive/shinylive.js" 71 | #' }, 72 | #' ... # [ truncated ] 73 | #' ] 74 | #' } 75 | #' ] 76 | #' ``` 77 | #' * `extension language-resources` 78 | #' * Prints the language-specific resource files as JSON that should be added to the quarto html dependency. 79 | #' * For r-shinylive, this includes the webr resource files 80 | #' * For py-shinylive, this includes the pyodide and pyright resource files. 81 | #' * Example 82 | #' ``` 83 | #' [ 84 | #' { 85 | #' "name": "shinylive/webr/esbuild.d.ts", 86 | #' "path": "//shinylive-0.2.0/shinylive/webr/esbuild.d.ts" 87 | #' }, 88 | #' { 89 | #' "name": "shinylive/webr/libRblas.so", 90 | #' "path": "//shinylive-0.2.0/shinylive/webr/libRblas.so" 91 | #' }, 92 | #' ... # [ truncated ] 93 | #' ] 94 | #' * `extension app-resources` 95 | #' * Prints app-specific resource files as JSON that should be added to the `"shinylive"` quarto html dependency. 96 | #' * Currently, r-shinylive does not return any resource files. 97 | #' * Example 98 | #' ``` 99 | #' [ 100 | #' { 101 | #' "name": "shinylive/pyodide/anyio-3.7.0-py3-none-any.whl", 102 | #' "path": "//shinylive-0.2.0/shinylive/pyodide/anyio-3.7.0-py3-none-any.whl" 103 | #' }, 104 | #' { 105 | #' "name": "shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl", 106 | #' "path": "//shinylive-0.2.0/shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl" 107 | #' }, 108 | #' ... # [ truncated ] 109 | #' ] 110 | #' ``` 111 | #' 112 | quarto_ext <- function( 113 | args = commandArgs(trailingOnly = TRUE), 114 | ..., 115 | pretty = is_interactive(), 116 | con = "stdin" 117 | ) { 118 | stopifnot(length(list(...)) == 0) 119 | # This method should not print anything to stdout. Instead, it should return a JSON string that will be printed by the extension. 120 | stopifnot(length(args) >= 1) 121 | 122 | followup_statement <- function() { 123 | c( 124 | i = "Please update your {.href [quarto-ext/shinylive](https://github.com/quarto-ext/shinylive)} Quarto extension for the latest integration.", 125 | i = "To update the shinylive extension, run this command in your Quarto project:", 126 | "\t{.code quarto add quarto-ext/shinylive}", 127 | "", 128 | "R shinylive package version: {.field {SHINYLIVE_R_VERSION}}", 129 | "Supported assets version: {.field {assets_version()}}" 130 | ) 131 | } 132 | 133 | # --version support 134 | if (args[1] == "--version") { 135 | cat(SHINYLIVE_R_VERSION, "\n") 136 | return(invisible()) 137 | } 138 | 139 | if (args[1] != "extension") { 140 | cli::cli_abort(c( 141 | "Unknown command: {.strong {args[1]}}. Expected {.var extension} as first argument", 142 | "", 143 | followup_statement() 144 | )) 145 | } 146 | 147 | methods <- list( 148 | "info" = "Package, version, asset version, and script paths information", 149 | "base-htmldeps" = "Quarto html dependencies for the base shinylive integration", 150 | "language-resources" = "R's resource files for the quarto html dependency named `shinylive`", 151 | "app-resources" = "App-specific resource files for the quarto html dependency named `shinylive`" 152 | ) 153 | 154 | not_enough_args <- length(args) < 2 155 | invalid_arg <- length(args) >= 2 && !(args[2] %in% names(methods)) 156 | 157 | if (not_enough_args || invalid_arg) { 158 | msg_stop <- 159 | if (not_enough_args) { 160 | "Missing {.var extension} subcommand" 161 | } else if (invalid_arg) { 162 | "Unknown {.var extension} subcommand {.strong {args[2]}}" 163 | } 164 | 165 | msg_methods <- c() 166 | for (method in names(methods)) { 167 | method_desc <- methods[[method]] 168 | msg_methods <- c( 169 | msg_methods, 170 | paste(cli::style_bold(method), "-", method_desc) 171 | ) 172 | } 173 | 174 | cli::cli_abort(c( 175 | msg_stop, 176 | "", 177 | cli::style_underline("Available methods"), 178 | msg_methods, 179 | "", 180 | followup_statement() 181 | )) 182 | } 183 | stopifnot(length(args) >= 2) 184 | 185 | ret <- switch( 186 | args[2], 187 | "info" = { 188 | list( 189 | "version" = SHINYLIVE_R_VERSION, 190 | "assets_version" = assets_version(), 191 | "scripts" = list( 192 | "codeblock-to-json" = quarto_codeblock_to_json_path() 193 | ) 194 | ) 195 | }, 196 | "base-htmldeps" = { 197 | sw_dir_pos <- which(args == "--sw-dir") 198 | if (length(sw_dir_pos) == 1) { 199 | if (sw_dir_pos == length(args)) { 200 | stop("expected `--sw-dir` argument value") 201 | } 202 | sw_dir <- args[sw_dir_pos + 1] 203 | } else { 204 | stop("expected `--sw-dir` argument") 205 | } 206 | # Language agnostic files 207 | shinylive_base_deps_htmldep(sw_dir) 208 | }, 209 | "language-resources" = { 210 | shinylive_r_resources() 211 | # shinylive_python_resources() 212 | }, 213 | "app-resources" = { 214 | app_json <- readLines(con, warn = FALSE) 215 | build_app_resources(app_json) 216 | }, 217 | { 218 | stop("Not implemented `extension` type: ", args[2]) 219 | } 220 | ) 221 | ret_null_free <- drop_nulls_rec(ret) 222 | ret_json <- jsonlite::toJSON( 223 | ret_null_free, 224 | pretty = pretty, 225 | auto_unbox = TRUE 226 | ) 227 | # Make sure the json is printed to stdout. 228 | # Do not rely on Rscript to print the last value. 229 | print(ret_json) 230 | 231 | # Return invisibly, so that nothing is printed 232 | invisible() 233 | } 234 | 235 | build_app_resources <- function(app_json) { 236 | projdir <- Sys.getenv("QUARTO_PROJECT_DIR", ".") 237 | appdir <- fs::path(projdir, ".quarto", "_webr", "appdir") 238 | destdir <- fs::path(projdir, ".quarto", "_webr", "destdir") 239 | 240 | # Build app directory, removing any previous app expanded there 241 | if (fs::dir_exists(appdir)) { 242 | fs::dir_delete(appdir) 243 | } 244 | fs::dir_create(appdir, recurse = TRUE) 245 | 246 | # Convert app.json into files on disk, so we can use `renv::dependencies()` 247 | app <- jsonlite::fromJSON( 248 | app_json, 249 | simplifyDataFrame = FALSE, 250 | simplifyMatrix = FALSE 251 | ) 252 | lapply(app, function(file) { 253 | file_name <- fs::path_norm(file$name) 254 | 255 | if (grepl("^(/|[.]{2})", file_name)) { 256 | cli::cli_abort(c( 257 | "App file paths must be relative to the app directory", 258 | x = "Invalid file path: {.path {file$name}}" 259 | )) 260 | } 261 | 262 | file_path <- fs::path(appdir, file_name) 263 | fs::dir_create(fs::path_dir(file_path)) 264 | 265 | if (file$type == "text") { 266 | writeLines(file$content, file_path) 267 | } else { 268 | try({ 269 | raw_content <- jsonlite::base64_dec(file$content) 270 | writeBin(raw_content, file_path, useBytes = TRUE) 271 | }) 272 | } 273 | }) 274 | 275 | wasm_packages <- sys_env_wasm_packages() 276 | if (wasm_packages && wasm_packages_able(assets_version())) { 277 | # Download wasm binaries ready to embed into Quarto deps 278 | withr::with_options( 279 | list(shinylive.quiet = TRUE), 280 | download_wasm_packages( 281 | appdir, 282 | destdir, 283 | package_cache = TRUE, 284 | max_filesize = NULL 285 | ) 286 | ) 287 | } 288 | 289 | # Enumerate R package Wasm binaries and prepare the VFS images as html deps 290 | webr_dir <- fs::path(destdir, "shinylive", "webr") 291 | packages_files <- dir(webr_dir, recursive = TRUE, full.names = FALSE) 292 | packages_paths <- file.path("shinylive", "webr", packages_files) 293 | packages_abs <- file.path(fs::path_abs(webr_dir), packages_files) 294 | 295 | Map( 296 | USE.NAMES = FALSE, 297 | packages_paths, 298 | packages_abs, 299 | f = function(rel_common_file, abs_common_file) { 300 | html_dep_obj( 301 | name = rel_common_file, 302 | path = abs_common_file 303 | ) 304 | } 305 | ) 306 | } 307 | 308 | quarto_codeblock_to_json_path <- function() { 309 | file.path(assets_dir(), "scripts", "codeblock-to-json.js") 310 | } 311 | 312 | # def package_deps(json_file: Optional[str]) -> None: 313 | # json_content: str | None = None 314 | # if json_file is None: 315 | # json_content = sys.stdin.read() 316 | 317 | # deps = _deps.package_deps_htmldepitems(json_file, json_content) 318 | # print(json.dumps(deps, indent=2)) 319 | -------------------------------------------------------------------------------- /R/assets.R: -------------------------------------------------------------------------------- 1 | #' Manage shinylive assets 2 | #' 3 | #' Helper methods for managing shinylive assets. 4 | #' 5 | #' @describeIn assets Downloads the shinylive assets bundle from GitHub and 6 | #' extracts it to the specified directory. The bundle will always be 7 | #' downloaded from GitHub, even if it already exists in the cache directory 8 | #' (`dir=`). 9 | #' @param version The version of the assets to download. 10 | #' @param ... Ignored. 11 | #' @param dir The asset cache directory. Unless testing, the default behavior 12 | #' should be used. 13 | #' @param url The URL to download the assets from. Unless testing, the default 14 | #' behavior should be used. 15 | #' @export 16 | #' @return 17 | #' `assets_version()` returns the version of the currently supported Shinylive. 18 | #' 19 | #' All other methods return `invisible()`. 20 | assets_download <- function( 21 | version = assets_version(), 22 | ..., 23 | # Note that this is the cache directory, which is the parent of the assets 24 | # directory. The tarball will have the assets directory as the top-level 25 | # subdir. 26 | dir = assets_cache_dir(), 27 | url = assets_bundle_url(version) 28 | ) { 29 | tmp_targz <- tempfile( 30 | paste0("shinylive-", gsub(".", "_", version, fixed = TRUE), "-"), 31 | fileext = ".tar.gz" 32 | ) 33 | 34 | on.exit( 35 | { 36 | if (fs::file_exists(tmp_targz)) { 37 | fs::file_delete(tmp_targz) 38 | } 39 | }, 40 | add = TRUE 41 | ) 42 | 43 | cli_progress_step("Downloading shinylive assets {.field v{version}}") 44 | req <- httr2::request(url) 45 | req <- httr2::req_progress(req) 46 | httr2::req_perform(req, path = tmp_targz) 47 | 48 | cli_progress_step("Unzipping shinylive assets to {.path {dir}}") 49 | fs::dir_create(dir) 50 | archive::archive_extract(tmp_targz, dir) 51 | 52 | cli_progress_done() 53 | invisible(dir) 54 | } 55 | 56 | 57 | # Returns the URL for the Shinylive assets bundle. 58 | assets_bundle_url <- function(version = assets_version()) { 59 | paste0( 60 | "https://github.com/posit-dev/shinylive/releases/download/", 61 | paste0("v", version), 62 | "/", 63 | paste0("shinylive-", version, ".tar.gz") 64 | ) 65 | } 66 | 67 | 68 | assets_cache_dir <- function() { 69 | # Must be normalized as `~` does not work with quarto 70 | cache_dir <- rappdirs::user_cache_dir("shinylive") 71 | if (!dir.exists(cache_dir)) { 72 | dir.create(cache_dir, recursive = TRUE) 73 | } 74 | normalizePath(cache_dir) 75 | } 76 | 77 | # Returns the directory used for caching Shinylive assets. This directory can 78 | # contain multiple versions of Shinylive assets. 79 | assets_cache_dir_exists <- function() { 80 | fs::dir_exists(assets_cache_dir()) 81 | } 82 | 83 | 84 | # Returns the directory containing cached Shinylive assets, for a particular 85 | # version of Shinylive. 86 | assets_dir <- function( 87 | version = assets_version(), 88 | ..., 89 | dir = assets_cache_dir() 90 | ) { 91 | assets_dir_impl(dir = assets_cache_dir(), version = version) 92 | } 93 | shinylive_prefix <- "shinylive-" 94 | assets_dir_impl <- function( 95 | ..., 96 | dir = assets_cache_dir(), 97 | version = assets_version() 98 | ) { 99 | stopifnot(length(list(...)) == 0) 100 | fs::path(dir, paste0(shinylive_prefix, version)) 101 | } 102 | 103 | 104 | install_local_helper <- function( 105 | ..., 106 | assets_repo_dir, 107 | install_fn = fs::file_copy, 108 | dir = assets_cache_dir(), 109 | version = package_json_version(assets_repo_dir) 110 | ) { 111 | stopifnot(length(list(...)) == 0) 112 | stopifnot(fs::dir_exists(assets_repo_dir)) 113 | repo_build_dir <- fs::path(assets_repo_dir, "build") 114 | if (!fs::dir_exists(repo_build_dir)) { 115 | cli::cli_abort(c( 116 | "Assets repo build dir does not exist ({.path {repo_build_dir}}).", 117 | i = "Have you called {.code make all} yet?" 118 | )) 119 | } 120 | target_dir <- assets_dir_impl(dir = dir, version = version) 121 | 122 | unlink_path(target_dir) 123 | install_fn(repo_build_dir, target_dir) 124 | 125 | if (version != assets_version()) { 126 | cli::cli_warn(c( 127 | "You are installing a local copy of shinylive assets that is not the same as the version used by the shinylive R package.", 128 | "Unexpected behavior may occur!", 129 | x = "New assets version: {version}", 130 | i = "Supported assets version: {assets_version()}" 131 | )) 132 | } 133 | } 134 | 135 | #' Install shinylive assets from from a local directory 136 | #' 137 | #' Helper methods for testing updates to shinylive assets. 138 | #' 139 | #' @describeIn install Copies all shinylive assets from a local shinylive 140 | #' repository (e.g. 141 | #' [`posit-dev/shinylive`](https://github.com/posit-dev/py-shinylive)). This 142 | #' must be repeated for any change in the assets. 143 | #' @param assets_repo_dir The local repository directory for shinylive assets 144 | #' (e.g. [`posit-dev/shinylive`](https://github.com/posit-dev/py-shinylive)) 145 | #' @param version The version of the assets being installed. 146 | #' @inheritParams assets_download 147 | #' @seealso [`assets_download()`], [`assets_ensure()`], [`assets_cleanup()`] 148 | #' @return All method return `invisible()`. 149 | #' @export 150 | assets_install_copy <- function( 151 | assets_repo_dir, 152 | ..., 153 | dir = assets_cache_dir(), 154 | version = package_json_version(assets_repo_dir) 155 | ) { 156 | install_local_helper( 157 | ..., 158 | install_fn = function(from, to) { 159 | fs::dir_create(to) 160 | fs::dir_copy(from, to, overwrite = TRUE) 161 | }, 162 | assets_repo_dir = assets_repo_dir, 163 | dir = dir, 164 | version = version 165 | ) 166 | 167 | invisible() 168 | } 169 | 170 | #' @describeIn install Creates a symlink of the local shinylive assets to the 171 | #' cached assets directory. After the first installation, the assets will the 172 | #' same as the source due to the symlink. 173 | #' @export 174 | assets_install_link <- function( 175 | assets_repo_dir, 176 | ..., 177 | dir = assets_cache_dir(), 178 | version = package_json_version(assets_repo_dir) 179 | ) { 180 | install_local_helper( 181 | ..., 182 | install_fn = function(from, to) { 183 | # Make sure from is an absolute path 184 | if (!fs::is_absolute_path(from)) { 185 | from <- fs::path_wd(from) 186 | } 187 | # Make sure parent folder exists 188 | fs::dir_create(fs::path_dir(to)) 189 | # Link dir 190 | fs::link_create(from, to) 191 | }, 192 | assets_repo_dir = assets_repo_dir, 193 | dir = dir, 194 | version = version 195 | ) 196 | 197 | invisible() 198 | } 199 | 200 | 201 | #' @describeIn assets Ensures a local copy of shinylive is installed. If a local 202 | #' copy of shinylive is not installed, it will be downloaded and installed. 203 | #' If a local copy of shinylive is installed, its path will be returned. 204 | #' @export 205 | assets_ensure <- function( 206 | version = assets_version(), 207 | ..., 208 | dir = assets_cache_dir(), 209 | url = assets_bundle_url(version) 210 | ) { 211 | stopifnot(length(list(...)) == 0) 212 | if (!fs::dir_exists(dir)) { 213 | cli_alert_info("Creating assets cache directory ", dir) 214 | fs::dir_create(dir) 215 | } 216 | 217 | assets_path <- assets_dir(version, dir = dir) 218 | if (!fs::dir_exists(assets_path)) { 219 | cli_alert_warning("{.path {assets_path}} assets directory does not exist.") 220 | assets_download(url = url, version = version, dir = dir) 221 | } 222 | 223 | invisible(assets_path) 224 | } 225 | 226 | 227 | # """Removes local copies of shinylive web assets, except for the one used by the 228 | # current version of the shinylive python package. 229 | 230 | # Parameters 231 | # ---------- 232 | # dir 233 | # The directory where shinylive is stored. If None, the default directory will 234 | # be used. 235 | # """ 236 | 237 | #' @describeIn assets Removes local copies of shinylive web assets, except for 238 | #' the one used by the current version of \pkg{shinylive}. 239 | #' @export 240 | assets_cleanup <- function( 241 | ..., 242 | dir = assets_cache_dir() 243 | ) { 244 | stopifnot(length(list(...)) == 0) 245 | versions <- vapply( 246 | assets_dirs(dir = dir), 247 | function(ver_path) { 248 | sub(shinylive_prefix, "", basename(ver_path)) 249 | }, 250 | character(1) 251 | ) 252 | if (assets_version() %in% versions) { 253 | cli_alert_info("Keeping version {assets_version()}") 254 | versions <- setdiff(versions, assets_version()) 255 | } 256 | 257 | if (length(versions) > 0) { 258 | assets_remove(versions, dir = dir) 259 | } 260 | 261 | invisible() 262 | } 263 | 264 | 265 | # """Removes local copy of shinylive. 266 | 267 | # Parameters 268 | # ---------- 269 | # shinylive_dir 270 | # The directory where shinylive is stored. If None, the default directory will 271 | # be used. 272 | 273 | # version 274 | # If a version is specified, only that version will be removed. 275 | # If None, all local versions except the version specified by SHINYLIVE_ASSETS_VERSION will be removed. 276 | # """ 277 | 278 | #' @describeIn assets Removes a local copies of shinylive web assets. 279 | #' @param versions The assets versions to remove. 280 | #' @export 281 | assets_remove <- function( 282 | versions, 283 | ..., 284 | dir = assets_cache_dir() 285 | ) { 286 | stopifnot(length(list(...)) == 0) 287 | stopifnot(length(versions) > 0 && is.character(versions)) 288 | 289 | lapply(versions, function(version) { 290 | target_dir <- assets_dir_impl(dir = dir, version = version) 291 | if (fs::dir_exists(target_dir)) { 292 | cli_progress_step("Removing {.path {target_dir}}") 293 | unlink_path(target_dir) 294 | } else { 295 | cli_alert_warning("{.path {target_dir}} folder does not exist") 296 | } 297 | }) 298 | 299 | invisible() 300 | } 301 | 302 | 303 | assets_dirs <- function( 304 | ..., 305 | dir = assets_cache_dir() 306 | ) { 307 | stopifnot(length(list(...)) == 0) 308 | if (!fs::dir_exists(dir)) { 309 | return(character(0)) 310 | } 311 | # fs::dir_ls(shinylive_dir, type = "directory", regexp = "^shinylive-") 312 | 313 | path_basenames <- 314 | # Using `dir()` to avoid the path expansion that `fs::dir_ls()` does. 315 | # `dir()` is 10x faster than `fs::dir_ls()` 316 | base::dir( 317 | dir, 318 | full.names = FALSE, 319 | pattern = paste0("^", shinylive_prefix) 320 | ) 321 | if (length(path_basenames) == 0) { 322 | return(character(0)) 323 | } 324 | 325 | # Sort descending by version numbers 326 | path_versions_str <- sub(shinylive_prefix, "", path_basenames) 327 | path_versions <- as.character( 328 | sort(numeric_version(path_versions_str), decreasing = TRUE) 329 | ) 330 | 331 | # Return full path to the versions 332 | fs::path(dir, paste0(shinylive_prefix, path_versions)) 333 | } 334 | 335 | 336 | #' @describeIn assets Prints information about the local shinylive assets that 337 | #' have been installed. Invisibly returns a table of installed asset versions 338 | #' and their associated paths. 339 | #' @param quiet In `assets_info()`, if `quiet = TRUE`, the function will not 340 | #' print the assets information to the console. 341 | #' @export 342 | assets_info <- function(quiet = FALSE) { 343 | installed_versions <- assets_dirs() 344 | if (length(installed_versions) == 0) { 345 | installed_versions <- "(None)" 346 | } 347 | 348 | local_quiet(quiet) 349 | 350 | cli_text("shinylive R package version: {.field {SHINYLIVE_R_VERSION}}") 351 | cli_text("shinylive web assets version: {.field {assets_version()}}") 352 | cli_text("") 353 | cli_text("Local cached shinylive asset dir:") 354 | cli_bullets(c(">" = "{.path {assets_cache_dir()}}")) 355 | cli_text("") 356 | cli_text("Installed assets:") 357 | if (assets_cache_dir_exists()) { 358 | cli_installed <- c() 359 | for (i in seq_along(installed_versions)) { 360 | cli_installed <- c( 361 | cli_installed, 362 | c("*" = sprintf("{.path {installed_versions[%s]}}", i)) 363 | ) 364 | } 365 | cli_bullets(cli_installed) 366 | } else { 367 | cli_bullets("(Cache dir does not exist)") 368 | } 369 | 370 | versions <- vapply( 371 | strsplit(installed_versions, "shinylive-", fixed = TRUE), 372 | FUN.VALUE = character(1), 373 | function(x) x[[2]] 374 | ) 375 | 376 | data <- data.frame( 377 | version = versions, 378 | path = installed_versions, 379 | is_assets_version = versions == assets_version() 380 | ) 381 | 382 | class(data) <- c("tbl_df", "tbl", "data.frame") 383 | 384 | if (is_quiet()) data else invisible(data) 385 | } 386 | 387 | 388 | #' @describeIn assets Returns the version of the currently supported Shinylive 389 | #' assets version. If the `SHINYLIVE_ASSETS_VERSION` environment variable is set, 390 | #' that value will be used. 391 | #' @export 392 | assets_version <- function() { 393 | Sys.getenv("SHINYLIVE_ASSETS_VERSION", SHINYLIVE_ASSETS_VERSION) 394 | } 395 | 396 | # """Checks if the URL for the Shinylive assets bundle is valid. 397 | 398 | # Returns True if the URL is valid (with a 200 status code), False otherwise. 399 | 400 | # The reason it has both the `version` and `url` parameters is so that it behaves the 401 | # same as `assets_download()` and `assets_ensure()`. 402 | # """ 403 | check_assets_url <- function( 404 | version = assets_version(), 405 | url = assets_bundle_url(version) 406 | ) { 407 | req <- httr2::request(url) 408 | req <- httr2::req_method(req, "HEAD") 409 | resp <- httr2::req_perform(req) 410 | resp$status_code == 200 411 | } 412 | -------------------------------------------------------------------------------- /R/packages.R: -------------------------------------------------------------------------------- 1 | SHINYLIVE_DEFAULT_MAX_FILESIZE <- "100MB" 2 | SHINYLIVE_WASM_PACKAGES <- TRUE 3 | 4 | # Sys env maximum filesize for asset bundling 5 | sys_env_max_filesize <- function() { 6 | max_fs_env <- Sys.getenv("SHINYLIVE_DEFAULT_MAX_FILESIZE") 7 | if (max_fs_env == "") NULL else max_fs_env 8 | } 9 | 10 | sys_env_wasm_packages <- function() { 11 | pkgs_env <- Sys.getenv("SHINYLIVE_WASM_PACKAGES", SHINYLIVE_WASM_PACKAGES) 12 | pkgs_env <- switch(pkgs_env, "1" = TRUE, "0" = FALSE, pkgs_env) 13 | wasm_packages <- as.logical(pkgs_env) 14 | if (is.na(wasm_packages)) { 15 | cli::cli_abort("Could not parse `wasm_packages` value: {.code {pkgs_env}}") 16 | } 17 | wasm_packages 18 | } 19 | 20 | # Resolve package list, dependencies listed in Depends and Imports 21 | resolve_dependencies <- function(pkgs, local = TRUE) { 22 | pkg_refs <- if (local) { 23 | refs <- find.package(pkgs, lib.loc = NULL, quiet = FALSE, !is_quiet()) 24 | glue::glue("local::{refs}") 25 | } else { 26 | pkgs 27 | } 28 | wasm_config <- list( 29 | platforms = "source", 30 | dependencies = list(c("Depends", "Imports"), c("Depends", "Imports")) 31 | ) 32 | inst <- pkgdepends::new_pkg_deps(pkg_refs, config = wasm_config) 33 | inst$resolve() 34 | unique(inst$get_resolution()$package) 35 | } 36 | 37 | check_repo_pkg_version <- function(desc, ver, pkg) { 38 | # Show a warning if packages major.minor versions differ 39 | # We don't worry too much about patch, since webR versions of packages may be 40 | # patched at the repo for compatibility with Emscripten 41 | inst_ver <- package_version(desc$Version) 42 | repo_ver <- package_version(ver) 43 | if (inst_ver$major != repo_ver$major || inst_ver$minor != repo_ver$minor) { 44 | cli_alert_warning( 45 | "{.pkg {pkg_wrap(pkg)}} [{cli::col_red(ver)}] does not match local version {desc$Version}" 46 | ) 47 | cli::cli_warn(c( 48 | "Package version mismatch for {.pkg {pkg}}, ensure the versions below are compatible.", 49 | "!" = "Installed version: {desc$Version}, WebAssembly version: {ver}.", 50 | "i" = "Install a package version matching the WebAssembly version to silence this error." 51 | )) 52 | } else if (!is_quiet()) { 53 | cli_alert_success("{.pkg {pkg_wrap(pkg)}} [{ver}]") 54 | } 55 | } 56 | 57 | get_wasm_assets <- function(desc, repo) { 58 | pkg <- desc$Package 59 | r_short <- gsub("\\.[^.]+$", "", WEBR_R_VERSION) 60 | contrib <- glue::glue("{repo}/bin/emscripten/contrib/{r_short}") 61 | 62 | info <- utils::available.packages(contriburl = contrib) 63 | if (!pkg %in% rownames(info)) { 64 | cli_alert_danger( 65 | "{.pkg {pkg_wrap(pkg)}} not available in Wasm binary repository: {.url {repo}}" 66 | ) 67 | cli::cli_warn( 68 | "Can't find {.pkg {pkg}} in Wasm binary repository: {.url {repo}}" 69 | ) 70 | return(list()) 71 | } 72 | 73 | ver <- info[pkg, "Version", drop = TRUE] 74 | check_repo_pkg_version(desc, ver, pkg) 75 | 76 | list( 77 | list( 78 | filename = glue::glue("{pkg}_{ver}.tgz"), 79 | url = glue::glue("{contrib}/{pkg}_{ver}.tgz") 80 | ) 81 | ) 82 | } 83 | 84 | get_github_wasm_assets <- function(desc) { 85 | pkg <- desc$Package 86 | user <- desc$RemoteUsername 87 | repo <- desc$RemoteRepo 88 | ref <- desc$RemoteRef 89 | 90 | # Find a release for installed package's RemoteRef 91 | tags <- tryCatch( 92 | gh::gh( 93 | "/repos/{user}/{repo}/releases/tags/{ref}", 94 | user = user, 95 | repo = repo, 96 | ref = ref 97 | ), 98 | error = function(err) { 99 | cli::cli_abort( 100 | c( 101 | "Can't find GitHub release for github::{user}/{repo}@{ref}", 102 | "!" = "Ensure a GitHub release exists for the package repository reference: {.val {ref}}.", 103 | "i" = "Alternatively, install a CRAN version of this package to use the default Wasm binary repository." 104 | ), 105 | parent = err 106 | ) 107 | } 108 | ) 109 | 110 | # Find GH release asset URLs for R library VFS image 111 | library_data <- Filter( 112 | function(item) { 113 | grepl("library.data", item$name, fixed = TRUE) 114 | }, 115 | tags$assets 116 | ) 117 | library_metadata <- Filter( 118 | function(item) { 119 | item$name == "library.js.metadata" 120 | }, 121 | tags$assets 122 | ) 123 | 124 | if (length(library_data) == 0 || length(library_metadata) == 0) { 125 | # We are stricter here than with CRAN-like repositories, the asset bundle 126 | # `RemoteRef` must match exactly. This allows for the use of development 127 | # versions of packages through the GitHub pre-releases feature. 128 | cli::cli_abort(c( 129 | "Can't find WebAssembly binary assets for github::{user}/{repo}@{ref}", 130 | "!" = "Ensure WebAssembly binary assets are associated with the GitHub release {.val {ref}}.", 131 | "i" = "WebAssembly binary assets can be built on release using GitHub Actions: {.url https://github.com/r-wasm/actions}", 132 | "i" = "Alternatively, install a CRAN version of this package to use the default Wasm binary repository." 133 | )) 134 | } 135 | 136 | list( 137 | list( 138 | filename = library_data[[1]]$name, 139 | url = library_data[[1]]$browser_download_url 140 | ), 141 | list( 142 | filename = library_metadata[[1]]$name, 143 | url = library_metadata[[1]]$browser_download_url 144 | ) 145 | ) 146 | } 147 | 148 | # Lookup URL and metadata for Wasm binary package 149 | prepare_wasm_metadata <- function(pkg, metadata) { 150 | desc <- utils::packageDescription(pkg) 151 | repo <- desc$Repository 152 | prev_ref <- metadata$ref 153 | prev_cached <- metadata$cached 154 | metadata$name <- pkg 155 | metadata$version <- desc$Version 156 | 157 | # Skip base R packages 158 | if (!is.null(desc$Priority) && desc$Priority == "base") { 159 | metadata$ref <- glue::glue("{metadata$name}@{metadata$version}") 160 | metadata$type <- "base" 161 | metadata$cached <- prev_cached <- TRUE 162 | cli_alert( 163 | "{pkg_wrap(metadata$name)} [{metadata$version}] skipping base R package" 164 | ) 165 | return(metadata) 166 | } 167 | 168 | # Set a package ref for caching 169 | if (!is.null(desc$RemoteType) && desc$RemoteType == "github") { 170 | user <- desc$RemoteUsername 171 | repo <- desc$RemoteRepo 172 | sha <- desc$RemoteSha 173 | metadata$ref <- glue::glue("github::{user}/{repo}@{sha}") 174 | } else if (is.null(repo) || repo == "CRAN") { 175 | repo <- "CRAN" 176 | metadata$ref <- glue::glue("{metadata$name}@{metadata$version}") 177 | } else if (grepl("Bioconductor", repo)) { 178 | metadata$ref <- glue::glue("bioc::{metadata$name}@{metadata$version}") 179 | } else if (grepl("r-universe\\.dev$", repo)) { 180 | metadata$ref <- glue::glue("{repo}::{metadata$name}@{desc$RemoteSha}") 181 | } else { 182 | metadata$ref <- glue::glue("{metadata$name}@{metadata$version}") 183 | } 184 | 185 | # If not cached, discover Wasm binary URLs 186 | if (is.null(prev_cached) || !prev_cached || prev_ref != metadata$ref) { 187 | metadata$cached <- FALSE 188 | if (!is.null(desc$RemoteType) && desc$RemoteType == "github") { 189 | metadata$assets <- get_github_wasm_assets(desc) 190 | metadata$type <- "library" 191 | } else if (grepl("r-universe\\.dev$", repo)) { 192 | metadata$assets <- get_wasm_assets(desc, repo = desc$Repository) 193 | metadata$type <- "package" 194 | } else if (grepl("Bioconductor", repo)) { 195 | # Use r-universe for Bioconductor packages 196 | metadata$assets <- get_wasm_assets( 197 | desc, 198 | repo = "https://bioc.r-universe.dev" 199 | ) 200 | metadata$type <- "package" 201 | } else { 202 | # Fallback to repo.r-wasm.org lookup for CRAN and anything else 203 | metadata$assets <- get_wasm_assets(desc, repo = "http://repo.r-wasm.org") 204 | metadata$type <- "package" 205 | } 206 | } 207 | 208 | metadata 209 | } 210 | 211 | # Dev usage: 212 | # withr::with_envvar(list(SHINYLIVE_DOWNLOAD_WASM_CORE_PACKAGES = "bslib"), {CODE}) 213 | env_download_wasm_core_packages <- function() { 214 | pkgs <- Sys.getenv("SHINYLIVE_DOWNLOAD_WASM_CORE_PACKAGES", "") 215 | 216 | if (!nzchar(pkgs)) { 217 | return() 218 | } 219 | 220 | strsplit(pkgs, "\\s*[ ,\n]\\s*")[[1]] 221 | } 222 | 223 | download_wasm_packages <- function( 224 | appdir, 225 | destdir, 226 | package_cache, 227 | max_filesize 228 | ) { 229 | max_filesize_missing <- is.null(sys_env_max_filesize()) && 230 | is.null(max_filesize) 231 | max_filesize_cli_fn <- if (max_filesize_missing) { 232 | cli::cli_warn 233 | } else { 234 | cli::cli_abort 235 | } 236 | 237 | max_filesize <- max_filesize %||% 238 | sys_env_max_filesize() %||% 239 | SHINYLIVE_DEFAULT_MAX_FILESIZE 240 | max_filesize <- if (is.na(max_filesize) || (max_filesize < 0)) { 241 | Inf 242 | } else { 243 | max_filesize 244 | } 245 | max_filesize_val <- max_filesize 246 | max_filesize <- fs::fs_bytes(max_filesize) 247 | if (is.na(max_filesize)) { 248 | cli::cli_warn(c( 249 | "!" = "Could not parse `max_filesize` value: {.code {max_filesize_val}}", 250 | "i" = "Setting to {.code {SHINYLIVE_DEFAULT_MAX_FILESIZE}}" 251 | )) 252 | max_filesize <- fs::fs_bytes(SHINYLIVE_DEFAULT_MAX_FILESIZE) 253 | } 254 | 255 | # Core packages in base webR image that we don't need to download 256 | shiny_pkgs <- c("shiny", "bslib", "renv") 257 | shiny_pkgs <- resolve_dependencies(shiny_pkgs, local = FALSE) 258 | 259 | # If a package appears in the download core allow list, 260 | # we remove it from the internal list of packages to skip downloading 261 | pkgs_download_core <- env_download_wasm_core_packages() 262 | if (length(pkgs_download_core) > 0) { 263 | shiny_pkgs <- setdiff(shiny_pkgs, pkgs_download_core) 264 | } 265 | 266 | # App dependencies, ignoring base webR + shiny packages 267 | pkgs_app <- unique(renv::dependencies(appdir, quiet = is_quiet())$Package) 268 | pkgs_app <- setdiff(pkgs_app, shiny_pkgs) 269 | 270 | # Create empty R packages directory in app assets if not already there 271 | pkg_dir <- fs::path(destdir, "shinylive", "webr", "packages") 272 | fs::dir_create(pkg_dir, recurse = TRUE) 273 | 274 | # Load existing metadata from disk, from a previously deployed app 275 | metadata_file <- fs::path( 276 | destdir, 277 | "shinylive", 278 | "webr", 279 | "packages", 280 | "metadata.rds" 281 | ) 282 | prev_metadata <- if (package_cache && fs::file_exists(metadata_file)) { 283 | readRDS(metadata_file) 284 | } else { 285 | list() 286 | } 287 | 288 | if (length(pkgs_app) > 0) { 289 | pkgs_app <- resolve_dependencies(pkgs_app) 290 | pkgs_app <- setdiff(pkgs_app, shiny_pkgs) 291 | names(pkgs_app) <- pkgs_app 292 | } 293 | 294 | if (!is_quiet()) { 295 | withr::local_options(cli.progress_show_after = 1) 296 | cli::cli_progress_bar( 297 | format = "{cli::pb_spin} Downloading R packages {cli::pb_bar} {cli::pb_current}/{cli::pb_total} | ETA: {cli::pb_eta} | {.pkg {pkg}}", 298 | format_done = "{cli::col_green(cli::symbol$tick)} Downloaded WASM binaries for {cli::pb_total} packages [{cli::pb_elapsed}]", 299 | total = length(pkgs_app), 300 | auto_terminate = FALSE, 301 | clear = FALSE 302 | ) 303 | } 304 | 305 | # Loop over packages and download them if not cached 306 | cur_metadata <- vector("list", length(pkgs_app)) 307 | names(cur_metadata) <- names(pkgs_app) 308 | 309 | withr::local_options(".shinylive.pkg_field_size" = max(nchar(pkgs_app))) 310 | 311 | for (i in seq_along(pkgs_app)) { 312 | pkg <- pkgs_app[i] 313 | 314 | if (!is_quiet()) { 315 | cli::cli_progress_update() 316 | } 317 | 318 | pkg_subdir <- fs::path(pkg_dir, pkg) 319 | fs::dir_create(pkg_subdir, recurse = TRUE) 320 | 321 | prev_meta <- if (pkg %in% names(prev_metadata)) { 322 | prev_metadata[[pkg]] 323 | } else { 324 | list() 325 | } 326 | # Create package ref and lookup download URLs 327 | meta <- prepare_wasm_metadata(pkg, prev_meta) 328 | 329 | if (!meta$cached) { 330 | # Download Wasm binaries and copy to static assets dir 331 | for (file in meta$assets) { 332 | path <- fs::path(pkg_subdir, file$filename) 333 | utils::download.file(file$url, path, mode = "wb", quiet = TRUE) 334 | 335 | # Disallow this package if an asset is too large 336 | if (fs::file_size(path) > max_filesize) { 337 | fs::dir_delete(pkg_subdir) 338 | meta$assets = list() 339 | max_filesize_cli_fn(c( 340 | "!" = "The file size of package {.pkg {pkg}} is larger than the maximum allowed file size of {.strong {max_filesize}}.", 341 | "!" = "This package will not be included as part of the WebAssembly asset bundle.", 342 | "i" = "Set the maximum allowed size to {.code -1}, {.code Inf}, or {.code NA} to disable this check.", 343 | "i" = if (max_filesize_missing) { 344 | "Explicitly set the maximum allowed size to treat this as an error." 345 | } else { 346 | NULL 347 | } 348 | )) 349 | break 350 | } 351 | } 352 | 353 | meta$cached <- TRUE 354 | meta$path <- NULL 355 | if (length(meta$assets) > 0) { 356 | meta$path <- glue::glue("packages/{pkg}/{meta$assets[[1]]$filename}") 357 | } 358 | } 359 | 360 | cur_metadata[[i]] <- meta 361 | } 362 | 363 | if (!is_quiet()) { 364 | cli::cli_progress_done() 365 | } 366 | 367 | # Merge metadata to protect previous cache 368 | pkgs <- unique(c(names(prev_metadata), names(cur_metadata))) 369 | metadata <- Map( 370 | function(a, b) if (is.null(b)) a else b, 371 | prev_metadata[pkgs], 372 | cur_metadata[pkgs] 373 | ) 374 | names(metadata) <- pkgs 375 | 376 | # Remove base packages from caching and metadata 377 | metadata <- Filter(function(item) item$type != "base", metadata) 378 | 379 | cli_progress_step("Writing app metadata to {.path {metadata_file}}") 380 | saveRDS(metadata, metadata_file) 381 | cli_progress_done() 382 | cli_alert_info( 383 | "Wrote {.path {metadata_file}} ({fs::file_info(metadata_file)$size[1]} bytes)" 384 | ) 385 | 386 | invisible(metadata_file) 387 | } 388 | 389 | pkg_wrap <- function(x) { 390 | width <- getOption(".shinylive.pkg_field_size", 20) 391 | cli::ansi_align(x, width = width, align = "left") 392 | } 393 | --------------------------------------------------------------------------------