├── .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 | [](https://github.com/posit-dev/r-shinylive/actions/workflows/R-CMD-check.yaml)
6 | [](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 |
--------------------------------------------------------------------------------