diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 20fb53e..286163b 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -32,17 +32,15 @@ jobs: python-version: "3.11" - name: Upgrade pip + shell: bash run: | python -m pip install --upgrade pip - name: Install py-shinylive + shell: bash run: | pip install shinylive - - name: Install assets for py-shinylive - run: | - shinylive assets info - shinylive assets download - shinylive assets info + # pip install https://github.com/posit-dev/py-shinylive/archive/split_api.zip - name: Set up Quarto uses: quarto-dev/quarto-actions/setup@v2 @@ -52,6 +50,8 @@ jobs: run: | cd local/quarto quarto add quarto-ext/shinylive --no-prompt + # Trouble installing from branch. Using url instead. + # quarto add http://github.com/quarto-ext/shinylive/archive/v2_api.zip --no-prompt - name: Install R, system dependencies, and package dependencies uses: rstudio/shiny-workflows/setup-r-package@v1 @@ -80,7 +80,10 @@ jobs: shell: Rscript {0} run: | shinylive_lua <- file.path( - "local", "quarto", "_extensions", "quarto-ext", "shinylive", "shinylive.lua" + "local", "quarto", "_extensions", + # (When installing from a zip url, there is no `quarto-ext` dir.) + "quarto-ext", + "shinylive", "shinylive.lua" ) shinylive_lua |> brio::read_file() |> @@ -90,13 +93,8 @@ jobs: fixed = TRUE ) |> sub( - pattern = " local deps = quarto.json.decode(depJson)", - replacement = " print(depJson)\n local deps = quarto.json.decode(depJson)", - fixed = TRUE - ) |> - sub( - pattern = " local appDeps = quarto.json.decode(appDepsJson)", - replacement = " print(appDepsJson)\n local appDeps = quarto.json.decode(appDepsJson)", + pattern = "-- print(\"res", + replacement = "-- print(\"res", fixed = TRUE ) |> brio::write_file(shinylive_lua) diff --git a/R/assets.R b/R/assets.R index 4cff833..ad045aa 100644 --- a/R/assets.R +++ b/R/assets.R @@ -1,4 +1,3 @@ - #' Manage shinylive assets #' #' Helper methods for managing shinylive assets. @@ -15,13 +14,12 @@ #' behavior should be used. #' @export assets_download <- function( - version = assets_version(), - ..., - # Note that this is the cache directory, which is the parent of the assets - # directory. The tarball will have the assets directory as the top-level subdir. - dir = assets_cache_dir(), - url = assets_bundle_url(version) -) { + version = assets_version(), + ..., + # Note that this is the cache directory, which is the parent of the assets + # directory. The tarball will have the assets directory as the top-level subdir. + dir = assets_cache_dir(), + url = assets_bundle_url(version)) { tmp_targz <- tempfile(paste0("shinylive-", gsub(".", "_", version, fixed = TRUE), "-"), fileext = ".tar.gz") on.exit( @@ -103,9 +101,10 @@ install_local_helper <- function( if (version != assets_version()) { message( - "Warning: You are installing a local copy of shinylive that is not the same as the version used by the shinylive R package.", + "Warning: You are installing a local copy of shinylive assets that is not the same as the version used by the shinylive R package.", "\nWarning: Unexpected behavior may occur!", - "\n\nNew version: ", version + "\n\nNew assets version: ", version, + "\nSupported assets version: ", assets_version() ) } } @@ -175,11 +174,10 @@ assets_install_link <- function( #' If a local copy of shinylive is installed, its path will be returned. #' @export assets_ensure <- function( - version = assets_version(), - ..., - dir = assets_cache_dir(), - url = assets_bundle_url(version) -) { + version = assets_version(), + ..., + dir = assets_cache_dir(), + url = assets_bundle_url(version)) { stopifnot(length(list(...)) == 0) if (!fs::dir_exists(dir)) { message("Creating assets cache directory ", dir) @@ -212,9 +210,8 @@ assets_ensure <- function( #' the one used by the current version of \pkg{shinylive}. #' @export assets_cleanup <- function( - ..., - dir = assets_cache_dir() -) { + ..., + dir = assets_cache_dir()) { stopifnot(length(list(...)) == 0) versions <- vapply( assets_dirs(dir = dir), @@ -254,10 +251,9 @@ assets_cleanup <- function( #' @param versions The assets versions to remove. #' @export assets_remove <- function( - versions, - ..., - dir = assets_cache_dir() -) { + versions, + ..., + dir = assets_cache_dir()) { stopifnot(length(list(...)) == 0) stopifnot(length(versions) > 0 && is.character(versions)) @@ -277,9 +273,8 @@ assets_remove <- function( assets_dirs <- function( - ..., - dir = assets_cache_dir() -) { + ..., + dir = assets_cache_dir()) { stopifnot(length(list(...)) == 0) if (!fs::dir_exists(dir)) { return(character(0)) @@ -322,7 +317,7 @@ assets_info <- function() { cat( collapse(c( - paste0("shinylive R package version: ", utils::packageVersion("shinylive")), + paste0("shinylive R package version: ", SHINYLIVE_R_VERSION), paste0("shinylive web assets version: ", assets_version()), "", "Local cached shinylive asset dir:", diff --git a/R/deps.R b/R/deps.R index 6576ed4..7f581db 100644 --- a/R/deps.R +++ b/R/deps.R @@ -8,12 +8,15 @@ html_dep_obj <- function( assert_nzchar_string(name) assert_nzchar_string(path) is.null(attribs) || assert_list(attribs) + ret <- list( + name = name, + path = path + ) + if (!is.null(attribs)) { + ret$attribs <- attribs + } structure( - list( - name = name, - path = path, - attribs = attribs - ), + ret, class = c(HTML_DEP_ITEM_CLASS, "list") ) } @@ -44,6 +47,7 @@ quarto_html_dependency_obj <- function( stylesheets = NULL, resources = NULL, meta = NULL, + head = NULL, serviceworkers = NULL) { stopifnot(length(list(...)) == 0) assert_nzchar_string(name) @@ -52,6 +56,7 @@ quarto_html_dependency_obj <- function( is.null(stylesheets) || assert_list_items(stylesheets, HTML_DEP_ITEM_CLASS) is.null(resources) || assert_list_items(resources, HTML_DEP_ITEM_CLASS) is.null(meta) || assert_list(meta) + is.null(head) || assert_nzchar_string(head) is.null(serviceworkers) || assert_list_items(serviceworkers, HTML_DEP_SERVICEWORKER_CLASS) @@ -63,6 +68,7 @@ quarto_html_dependency_obj <- function( stylesheets = stylesheets, resources = resources, meta = meta, + head = head, serviceworkers = serviceworkers ), class = c(QUARTO_HTML_DEPENDENCY_CLASS, "list") @@ -72,9 +78,17 @@ quarto_html_dependency_obj <- function( shinylive_base_deps_htmldep <- function(sw_dir = NULL) { list( serviceworker_dep(sw_dir), - shinylive_common_dep_htmldep() + shinylive_common_dep_htmldep("base") ) } +shinylive_r_resources <- function() { + shinylive_common_dep_htmldep("r")$resources +} +# Not used in practice! +shinylive_python_resources <- function(sw_dir = NULL) { + shinylive_common_dep_htmldep("python")$resources +} + serviceworker_dep <- function(sw_dir) { quarto_html_dependency_obj( @@ -88,7 +102,8 @@ serviceworker_dep <- function(sw_dir) { ), meta = if (!is.null(sw_dir)) { - # Add meta tag to tell load-shinylive-sw.js where to find shinylive-sw.js. + # Add meta tag to tell load-shinylive-sw.js where to find + # shinylive-sw.js. list("shinylive:serviceworker_dir" = sw_dir) } else { NULL @@ -102,82 +117,106 @@ serviceworker_dep <- function(sw_dir) { # dependencies; in other words, the files that are always included in a # Shinylive deployment. # """ -shinylive_common_dep_htmldep <- function() { +shinylive_common_dep_htmldep <- function(dep_type = c("base", "python", "r")) { assets_path <- assets_dir() # In quarto ext, keep support for python engine - base_files <- shinylive_common_files(all_files = TRUE) - - scripts <- list() - stylesheets <- list() - resources <- list() - - add_item <- function( - type = c("script", "stylesheet", "resource"), - name, - path, - attribs = NULL) { - dep_item <- html_dep_obj(name = name, path = path, attribs = attribs) - switch(match.arg(type), - "script" = { - scripts[[length(scripts) + 1]] <<- dep_item - }, - "stylesheet" = { - stylesheets[[length(stylesheets) + 1]] <<- dep_item - }, - "resource" = { - resources[[length(resources) + 1]] <<- dep_item - }, - { - stop("unknown type: ", type) - } - ) - } + rel_common_files <- shinylive_common_files(dep_type = dep_type) + abs_common_files <- file.path(assets_path, rel_common_files) - lapply(base_files, function(base_file) { - base_file_basename <- basename(base_file) - if ( - base_file_basename == "load-shinylive-sw.js" || - base_file_basename == "run-python-blocks.js" - ) { - add_item( - type = "script", - name = base_file, - path = file.path(assets_path, base_file), - attribs = list(type = "module") - ) - } else if (base_file_basename == "shinylive.css") { - add_item( - type = "stylesheet", - name = base_file, - path = file.path(assets_path, base_file) + # `NULL` values can be inserted into; + # Ex: `a <- NULL; a[[1]] <- 4; stopifnot(identical(a, list(4)))` + scripts <- NULL + stylesheets <- NULL + resources <- NULL + + switch(dep_type, + "python" = , + "r" = { + # Language specific files are all resources + # For speed / simplicity, create deps directly + resources <- Map( + USE.NAMES = FALSE, + rel_common_files, + abs_common_files, + f = function(rel_common_file, abs_common_file) { + html_dep_obj( + name = rel_common_file, + path = abs_common_file + ) + } ) - } else { - add_item( - type = "resource", - name = base_file, - path = file.path(assets_path, base_file) + }, + "base" = { + # Placeholder for load-shinylive-sw.js; (Existance is validated later) + load_shinylive_dep <- NULL + # Placeholder for run-python-blocks.js; Appended to end of scripts + run_python_blocks_dep <- NULL + + Map( + rel_common_files, + abs_common_files, + basename(rel_common_files), + f = function(rel_common_file, abs_common_file, common_file_basename) { + switch(common_file_basename, + "run-python-blocks.js" = { + run_python_blocks_dep <<- + html_dep_obj( + name = rel_common_file, + path = abs_common_file, + attribs = list(type = "module") + ) + }, + "load-shinylive-sw.js" = { + load_shinylive_dep <<- + html_dep_obj( + name = rel_common_file, + path = abs_common_file, + attribs = list(type = "module") + ) + }, + "shinylive.css" = { + stylesheets[[length(stylesheets) + 1]] <<- + html_dep_obj( + name = rel_common_file, + path = abs_common_file + ) + }, + { + # Resource file + resources[[length(resources) + 1]] <<- + html_dep_obj( + name = rel_common_file, + path = abs_common_file + ) + } + ) + # Do not return anything + NULL + } ) - } - }) - # # Add base python packages as resources - # resources.extend(base_package_deps_htmldepitems()) - - # Sort scripts so that load-serviceworker.js is first, and - # run-python-blocks.js is last. - scripts_names <- vapply(scripts, `[[`, character(1), "name") - scripts <- c( - scripts[scripts_names == "load-serviceworker.js"], - scripts[scripts_names != "load-serviceworker.js"] - ) - scripts_names <- vapply(scripts, `[[`, character(1), "name") - scripts <- c( - scripts[scripts_names != "run-python-blocks.js"], - scripts[scripts_names == "run-python-blocks.js"] + # Put load-shinylive-sw.js in the scripts first + if (is.null(load_shinylive_dep)) { + stop("load-shinylive-sw.js not found in assets") + } + scripts <- c(list(load_shinylive_dep), scripts) + + # Append run_python_blocks_dep if it exists + if (!is.null(run_python_blocks_dep)) { + scripts[[length(scripts) + 1]] <- run_python_blocks_dep + } + }, + { + stop("unknown dep_type: ", dep_type) + } ) + # # Add base python packages as resources + # python: `resources.extend(base_package_deps_htmldepitems())` + quarto_html_dependency_obj( + # MUST be called `"shinylive"` to match quarto ext name name = "shinylive", version = SHINYLIVE_ASSETS_VERSION, scripts = scripts, @@ -191,30 +230,65 @@ shinylive_common_dep_htmldep <- function() { # Return a list of files that are base dependencies; in other words, the files # that are always included in a Shinylive deployment. # """ -shinylive_common_files <- function(all_files = FALSE) { +shinylive_common_files <- function(dep_type = c("base", "python", "r")) { + dep_type <- match.arg(dep_type) assets_ensure() - assets_dir <- assets_dir() - # `dir()` is 10x faster than `fs::dir_ls()` - common_files <- dir(assets_dir, recursive = TRUE) + assets_folder <- assets_dir() + # # `dir()` is 10x faster than `fs::dir_ls()` + # common_files <- dir(assets_folder, recursive = TRUE) - if (!all_files) { - common_files <- common_files[!grepl("^shinylive/pyodide/", common_files)] - common_files <- common_files[!grepl("^shinylive/pyright/", common_files)] + asset_files_in_folder <- function(assets_sub_path, recurse) { + folder <- + if (is.null(assets_sub_path)) { + assets_folder + } else { + file.path(assets_folder, assets_sub_path) + } + files <- dir(folder, recursive = recurse, full.names = FALSE) + rel_files <- + if (is.null(assets_sub_path)) { + files + } else { + file.path(assets_sub_path, files) + } + if (recurse) { + # Does not contain dirs by definition + # Return as is + rel_files + } else { + # Remove directories + rel_files[!file.info(file.path(folder, files))$isdir] + } } - common_files <- common_files[!grepl("^scripts/", common_files)] - common_files <- common_files[!grepl("^export_template/", common_files)] - common_files <- setdiff(common_files, "shinylive/examples.json") - - # Return relative path to the assets_dir - fs::path(common_files) -} - - - - - - + common_files <- + switch(dep_type, + "base" = { + # Do copy any "top-level" python files as they are minimal + c( + # Do not include `./scripts` or `./export_template` in base deps + asset_files_in_folder(NULL, recurse = FALSE), + # Do not include `./shinylive/examples.json` in base deps + setdiff(asset_files_in_folder("shinylive", recurse = FALSE), "shinylive/examples.json") + ) + }, + "r" = { + c( + asset_files_in_folder(file.path("shinylive", "webr"), recurse = TRUE) + ) + }, + "python" = { + c( + asset_files_in_folder(file.path("shinylive", "pyodide"), recurse = TRUE), + asset_files_in_folder(file.path("shinylive", "pyright"), recurse = TRUE) + ) + }, + { + stop("unknown dep_type: ", dep_type) + } + ) -# Also implement `package_deps_htmldepitems`? Return empty list? + # Return relative path to the assets in `assets_dir()` + common_files +} diff --git a/R/export.R b/R/export.R index 140ec97..3a16af5 100644 --- a/R/export.R +++ b/R/export.R @@ -69,7 +69,7 @@ export <- function( "Copying base Shinylive files from ", assets_path, "/ to ", destdir, "/" ) # When exporting, we know it is only an R app. So remove python support - base_files <- shinylive_common_files(all_files = FALSE) + base_files <- c(shinylive_common_files("base"), shinylive_common_files("r")) if (verbose) { p <- progress::progress_bar$new( format = "[:bar] :percent", diff --git a/R/quarto_ext.R b/R/quarto_ext.R index 736352b..fee7a6a 100644 --- a/R/quarto_ext.R +++ b/R/quarto_ext.R @@ -1,44 +1,225 @@ +#' TODO-barret; update docs in py-shinylive + + #' Quarto extension for shinylive #' -#' @param args Command line arguments passed by the extension. The first -#' argument can be one of: +#' Integration with https://github.com/quarto-ext/shinylive +#' +#' @param args Command line arguments passed by the extension. See details for more information. +#' @param ... Ignored. +#' @param pretty Whether to pretty print the JSON output. +#' @returns Nothing. Values are printed to stdout. +#' @section Command arguments: #' -#' - `codeblock-to-json-path`: Prints the path to the `codeblock-to-json.js` script. -#' - `base-deps`: Prints the base dependencies as a JSON array. -#' - `package-deps`: Prints the package dependencies as a JSON array. -#' Currently, this returns an empty array as `webr` is handling the package -#' dependencies. -#' @noRd -quarto_ext <- function(args = commandArgs(trailingOnly = TRUE)) { +#' The first argument must be `"extension"`. This is done to match +#' `py-shinylive` so that it can nest other sub-commands under the `extension` +#' argument to minimize the api clutter the user can see. +#' +#' ### CLI Interface +#' * `extension info` +#' * Prints information about the extension including: +#' * `version`: The version of the R package +#' * `assets_version`: The version of the web assets +#' * `scripts`: A list of paths scripts that are used by the extension, +#' mainly `codeblock-to-json` +#' * Example +#' ``` +#' { +#' "version": "0.1.0", +#' "assets_version": "0.2.0", +#' "scripts": { +#' "codeblock-to-json": "//shinylive-0.2.0/scripts/codeblock-to-json.js" +#' } +#' } +#' ``` +#' * `extension base-htmldeps` +#' * Prints the language agnostic quarto html dependencies as a JSON array. +#' * The first html dependency is the `shinylive` service workers. +#' * The second html dependency is the `shinylive` base dependencies. This +#' dependency will contain the core `shinylive` asset scripts (JS files +#' automatically sourced), stylesheets (CSS files that are automatically +#' included), and resources (additional files that the JS and CSS files can +#' source). +#' * Example +#' ``` +#' [ +#' { +#' "name": "shinylive-serviceworker", +#' "version": "0.2.0", +#' "meta": { "shinylive:serviceworker_dir": "." }, +#' "serviceworkers": [ +#' { +#' "source": "//shinylive-0.2.0/shinylive-sw.js", +#' "destination": "/shinylive-sw.js" +#' } +#' ] +#' }, +#' { +#' "name": "shinylive", +#' "version": "0.2.0", +#' "scripts": [{ +#' "name": "shinylive/load-shinylive-sw.js", +#' "path": "//shinylive-0.2.0/shinylive/load-shinylive-sw.js", +#' "attribs": { "type": "module" } +#' }], +#' "stylesheets": [{ +#' "name": "shinylive/shinylive.css", +#' "path": "//shinylive-0.2.0/shinylive/shinylive.css" +#' }], +#' "resources": [ +#' { +#' "name": "shinylive/shinylive.js", +#' "path": "//shinylive-0.2.0/shinylive/shinylive.js" +#' }, +#' ... # [ truncated ] +#' ] +#' } +#' ] +#' ``` +#' * `extension language-resources` +#' * Prints the language-specific resource files as JSON that should be added to the quarto html dependency. +#' * For r-shinylive, this includes the webr resource files +#' * For py-shinylive, this includes the pyodide and pyright resource files. +#' * Example +#' ``` +#' [ +#' { +#' "name": "shinylive/webr/esbuild.d.ts", +#' "path": "//shinylive-0.2.0/shinylive/webr/esbuild.d.ts" +#' }, +#' { +#' "name": "shinylive/webr/libRblas.so", +#' "path": "//shinylive-0.2.0/shinylive/webr/libRblas.so" +#' }, +#' ... # [ truncated ] +#' ] +#' * `extension app-resources` +#' * Prints app-specific resource files as JSON that should be added to the `"shinylive"` quarto html dependency. +#' * Currently, r-shinylive does not return any resource files. +#' * Example +#' ``` +#' [ +#' { +#' "name": "shinylive/pyodide/anyio-3.7.0-py3-none-any.whl", +#' "path": "//shinylive-0.2.0/shinylive/pyodide/anyio-3.7.0-py3-none-any.whl" +#' }, +#' { +#' "name": "shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl", +#' "path": "//shinylive-0.2.0/shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl" +#' }, +#' ... # [ truncated ] +#' ] +#' ``` +#' +#' @importFrom rlang is_interactive +quarto_ext <- function( + args = commandArgs(trailingOnly = TRUE), + ..., + pretty = is_interactive()) { + stopifnot(length(list(...)) == 0) # This method should not print anything to stdout. Instead, it should return a JSON string that will be printed by the extension. stopifnot(length(args) >= 1) - ret <- switch(args[1], - "codeblock-to-json-path" = { - ret <- quarto_codeblock_to_json_path() - cat(ret, "\n", sep = "") - return(invisible()) + followup_statement <- function() { + paste0( + "Please update your `quarto-ext/shinylive` Quarto extension for the latest integration.\n", + "To update the shinylive extension, run this command in your Quarto project:\n", + "\tquarto add quarto-ext/shinylive\n", + "\n", + paste0("R shinylive package version: ", SHINYLIVE_R_VERSION), "\n", + paste0("Supported assets version: ", assets_version()) + ) + } + + # --version support + if (args[1] == "--version") { + cat(SHINYLIVE_R_VERSION, "\n") + return(invisible()) + } + + if (args[1] != "extension") { + stop( + "Unknown command: '", args[1], "'\n", + "Expected `extension` as first argument\n", + "\n", + followup_statement() + ) + } + + + if ( + (not_enough_args <- length(args) < 2) || + (invalid_arg <- !(args[2] %in% c( + "info", + "base-htmldeps", + "language-resources", + "app-resources" + ))) + ) { + stop( + if (not_enough_args) { + "Missing `extension` subcommand\n" + } else if (invalid_arg) { + paste0("Unknown `extension` subcommand: '", args[2], "'\n") + }, + "Known methods:\n", + paste0( + " ", + c( + "info - Package, version, asset version, and script paths information", + "base-htmldeps - Quarto html dependencies for the base shinylive integration", + "language-resources - R's resource files for the quarto html dependency named `shinylive`", + "app-resources - App-specific resource files for the quarto html dependency named `shinylive`" + ), + collapse = "\n" + ), + "\n\n", + followup_statement() + ) + } + stopifnot(length(args) >= 2) + + ret <- switch(args[2], + "info" = { + list( + "version" = SHINYLIVE_R_VERSION, + "assets_version" = SHINYLIVE_ASSETS_VERSION, + "scripts" = list( + "codeblock-to-json" = quarto_codeblock_to_json_path() + ) + ) }, - "base-deps" = { + "base-htmldeps" = { sw_dir_pos <- which(args == "--sw-dir") if (length(sw_dir_pos) == 1) { + if (sw_dir_pos == length(args)) { + stop("expected `--sw-dir` argument value") + } sw_dir <- args[sw_dir_pos + 1] } else { stop("expected `--sw-dir` argument") } + # Language agnostic files shinylive_base_deps_htmldep(sw_dir) }, - "package-deps" = { + "language-resources" = { + shinylive_r_resources() + # shinylive_python_resources() + }, + "app-resources" = { list() }, { - stop("unknown command: ", args[1]) + stop("Not implemented `extension` type: ", args[2]) } ) ret_null_free <- drop_nulls_rec(ret) - ret_json <- jsonlite::toJSON(ret_null_free, pretty = TRUE, auto_unbox = TRUE) + ret_json <- jsonlite::toJSON(ret_null_free, pretty = pretty, auto_unbox = TRUE) + # Make sure the json is printed to stdout. + # Do not rely on Rscript to print the last value. print(ret_json) + # Return invisibly, so that nothing is printed invisible() } diff --git a/R/version.R b/R/version.R index bd7b30f..8bc70b8 100644 --- a/R/version.R +++ b/R/version.R @@ -1,2 +1,3 @@ # This is the version of the Shinylive assets to use. -SHINYLIVE_ASSETS_VERSION = "0.2.0" +SHINYLIVE_ASSETS_VERSION <- "0.2.1" +SHINYLIVE_R_VERSION <- as.character(utils::packageVersion("shinylive")) diff --git a/README.md b/README.md index 7e43736..a28d267 100644 --- a/README.md +++ b/README.md @@ -63,35 +63,18 @@ shinylive::export("myapp2", "site", subdir = "app2") ## R package availability -To tell `{webr}` that you need to use a package outside of `{shiny}` and its dependencies, call `webr::install("CRAN_PKG")` in your Quarto `shinylive-r` code block as below or within your `app.R` file if you are exporting your Shiny app. Once the package has been installed by `{webr}`, you can use `library(CRAN_PKG)` or `require(CRAN_PKG)` as you desire. +The `{shinylive}` web assets will statically inspect which packages are being used via `{renv}`. -```` markdown - - -# Shinylive example chunk that uses `{plotly}` and `{DT}` - -```{shinylive-r} -#| standalone: true -#| components: [editor, viewer] - -# Install {plotly} and {DT} via {webr} -webr::install("plotly") -webr::install("DT") - -# Use plotly, DT, and shiny like normal -library("shiny") -library("plotly") -library("DT") - -## INSERT `ui` and `server` CODE HERE ## -ui <- .... -server <- .... - -shinyApp(ui, server) +If you need a package that can not be found by `{renv}`, add an impossible-to-reach code within your Shiny application that has a library call to that R package. For example: +```r +if (FALSE) { + library(HIDDEN_CRAN_PKG) +} ``` -```` -If a package has trouble loading, visit https://repo.r-wasm.org/ to see if it is able to be installed as a precompiled WebAssembly binary. +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")`. + +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. > [Note from `{webr}`](https://docs.r-wasm.org/webr/latest/packages.html#building-r-packages-for-webr):
> 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. @@ -105,15 +88,15 @@ To see which version of this R package you have, and which version of the web as ``` r shinylive::assets_info() -#> shinylive R package version: 0.0.1 -#> shinylive web assets version: 0.1.7 +#> shinylive R package version: 0.1.0 +#> shinylive web assets version: 0.2.1 #> #> Local cached shinylive asset dir: #> /Users/username/Library/Caches/shinylive #> #> Installed assets: -#> /Users/username/Library/Caches/shinylive/0.1.7 -#> /Users/username/Library/Caches/shinylive/0.1.6 +#> /Users/username/Library/Caches/shinylive/0.2.1 +#> /Users/username/Library/Caches/shinylive/0.2.0 ``` 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. @@ -128,26 +111,20 @@ You can remove old versions with `shinylive::assets_cleanup()`. This will remove ``` r shinylive::assets_cleanup() -#> Keeping version 0.1.7 -#> Removing /Users/username/Library/Caches/shinylive/0.1.6 +#> Keeping version 0.2.1 +#> Removing /Users/username/Library/Caches/shinylive/0.2.0 #> Removing /Users/username/Library/Caches/shinylive/0.1.5 ``` To remove a specific version, use `shinylive::assets_remove()`: ``` r -shinylive::assets_remove("0.1.5") -#> Removing /Users/username/Library/Caches/shinylive/0.1.5 +shinylive::assets_remove("0.2.1") +#> Removing /Users/username/Library/Caches/shinylive/0.2.1 ``` ## Known limitations -* A single quarto document can have both `shinylive-python` and `shinylive-r` code blocks, but `shinylive-r` code block must come first. - * Details: Only the first shinylive code block will be initialized. Currently `posit-dev/shinylive-py` does not know about `shinylive-r` code blocks. - * Details: This should be (naturally) fixed in the next release of `posit-dev/shinylive-py`. -* The current R common files contain files for python's pyodide and pyright when used within the quarto extension. - * Details: If only R files are used, these python files should be removed for smaller bundles / faster loading. - * Details: Currently, the extension does not know if there are more chunks with python code, so `r-shinylive` includes the `py-shinylive` asset files by default. * [Note from `{webr}`](https://docs.r-wasm.org/webr/latest/packages.html#building-r-packages-for-webr): * > 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,7 +133,7 @@ shinylive::assets_remove("0.1.5") ### Setup - shinylive assets -Works with latest GitHub version of [`posit-dev/shinylive`](https://github.com/posit-dev/shinylive/) (>= v`0.2.0`). +Works with latest GitHub version of [`posit-dev/shinylive`](https://github.com/posit-dev/shinylive/) (>= v`0.2.1`). Before linking the shinylive assets to the asset cache folder, you must first build the shiny live assets: diff --git a/inst/WORDLIST b/inst/WORDLIST index de11dac..66a33a0 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -5,3 +5,17 @@ pyright symlink wasm webR +api +barret +CLI +github +https +JS +pre +precompiled +py +stdout +stylesheets +toolchain +WebAssembly +webr diff --git a/local/quarto/shinylive.qmd b/local/quarto/shinylive.qmd index 0a79072..388cf74 100644 --- a/local/quarto/shinylive.qmd +++ b/local/quarto/shinylive.qmd @@ -2,8 +2,7 @@ title: Shinylive applications embedded in Quarto documents format: html filters: - # quarto-ext/shinylive - - shinylive + - quarto-ext/shinylive --- # `R` @@ -16,19 +15,18 @@ library(shiny) shinyApp( fluidPage( - sliderInput("n", "N", 0, 100, 40), + sliderInput("n", "N", 0, 100, 20), verbatimTextOutput("txt", placeholder = TRUE), ), function(input, output) { output$txt <- renderText({ - paste0("The value of n*2 is ", 2 * input$n) + paste0("n*2 is ", 2 * input$n) }) } ) ``` ::: - # `python` :::{.column-page-inset-right} diff --git a/man/quarto_ext.Rd b/man/quarto_ext.Rd new file mode 100644 index 0000000..ff3437b --- /dev/null +++ b/man/quarto_ext.Rd @@ -0,0 +1,147 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/quarto_ext.R +\name{quarto_ext} +\alias{quarto_ext} +\title{TODO-barret; update docs in py-shinylive +Quarto extension for shinylive} +\usage{ +quarto_ext( + args = commandArgs(trailingOnly = TRUE), + ..., + pretty = is_interactive() +) +} +\arguments{ +\item{args}{Command line arguments passed by the extension. See details for more information.} + +\item{...}{Ignored.} + +\item{pretty}{Whether to pretty print the JSON output.} +} +\value{ +Nothing. Values are printed to stdout. +} +\description{ +Integration with https://github.com/quarto-ext/shinylive +} +\section{Command arguments}{ + + +The first argument must be \code{"extension"}. This is done to match +\code{py-shinylive} so that it can nest other sub-commands under the \code{extension} +argument to minimize the api clutter the user can see. +\subsection{CLI Interface}{ +\itemize{ +\item \verb{extension info} +\itemize{ +\item Prints information about the extension including: +\itemize{ +\item \code{version}: The version of the R package +\item \code{assets_version}: The version of the web assets +\item \code{scripts}: A list of paths scripts that are used by the extension, +mainly \code{codeblock-to-json} +} +\item Example + +\if{html}{\out{
}}\preformatted{\{ + "version": "0.1.0", + "assets_version": "0.2.0", + "scripts": \{ + "codeblock-to-json": "//shinylive-0.2.0/scripts/codeblock-to-json.js" + \} +\} +}\if{html}{\out{
}} +} +\item \verb{extension base-htmldeps} +\itemize{ +\item Prints the language agnostic quarto html dependencies as a JSON array. +\itemize{ +\item The first html dependency is the \code{shinylive} service workers. +\item The second html dependency is the \code{shinylive} base dependencies. This +dependency will contain the core \code{shinylive} asset scripts (JS files +automatically sourced), stylesheets (CSS files that are automatically +included), and resources (additional files that the JS and CSS files can +source). +} +\item Example + +\if{html}{\out{
}}\preformatted{[ + \{ + "name": "shinylive-serviceworker", + "version": "0.2.0", + "meta": \{ "shinylive:serviceworker_dir": "." \}, + "serviceworkers": [ + \{ + "source": "//shinylive-0.2.0/shinylive-sw.js", + "destination": "/shinylive-sw.js" + \} + ] + \}, + \{ + "name": "shinylive", + "version": "0.2.0", + "scripts": [\{ + "name": "shinylive/load-shinylive-sw.js", + "path": "//shinylive-0.2.0/shinylive/load-shinylive-sw.js", + "attribs": \{ "type": "module" \} + \}], + "stylesheets": [\{ + "name": "shinylive/shinylive.css", + "path": "//shinylive-0.2.0/shinylive/shinylive.css" + \}], + "resources": [ + \{ + "name": "shinylive/shinylive.js", + "path": "//shinylive-0.2.0/shinylive/shinylive.js" + \}, + ... # [ truncated ] + ] + \} +] +}\if{html}{\out{
}} +} +\item \verb{extension language-resources} +\itemize{ +\item Prints the language-specific resource files as JSON that should be added to the quarto html dependency. +\itemize{ +\item For r-shinylive, this includes the webr resource files +\item For py-shinylive, this includes the pyodide and pyright resource files. +} +\item Example + +\if{html}{\out{
}}\preformatted{[ + \{ + "name": "shinylive/webr/esbuild.d.ts", + "path": "//shinylive-0.2.0/shinylive/webr/esbuild.d.ts" + \}, + \{ + "name": "shinylive/webr/libRblas.so", + "path": "//shinylive-0.2.0/shinylive/webr/libRblas.so" + \}, + ... # [ truncated ] +] +}\if{html}{\out{
}} +} +\item \verb{extension app-resources} +\itemize{ +\item Prints app-specific resource files as JSON that should be added to the \code{"shinylive"} quarto html dependency. +\item Currently, r-shinylive does not return any resource files. +\item Example + +\if{html}{\out{
}}\preformatted{[ + \{ + "name": "shinylive/pyodide/anyio-3.7.0-py3-none-any.whl", + "path": "//shinylive-0.2.0/shinylive/pyodide/anyio-3.7.0-py3-none-any.whl" + \}, + \{ + "name": "shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl", + "path": "//shinylive-0.2.0/shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl" + \}, + ... # [ truncated ] +] +}\if{html}{\out{
}} +} +} +} +} + diff --git a/tests/testthat/test-quarto_ext.R b/tests/testthat/test-quarto_ext.R index 768a9b3..40f2a61 100644 --- a/tests/testthat/test-quarto_ext.R +++ b/tests/testthat/test-quarto_ext.R @@ -1,43 +1,70 @@ - - -test_that("quarto_ext handles codeblock-to-json-path", { +test_that("quarto_ext handles `extension info`", { maybe_skip_test() assets_ensure() txt <- collapse(capture.output({ - quarto_ext(c("codeblock-to-json-path")) + quarto_ext(c("extension", "info")) })) - expect_true(grepl("/scripts/codeblock-to-json\\.js$", txt)) + info <- jsonlite::parse_json(txt) + expect_equal(info$version, as.character(utils::packageVersion("shinylive"))) + expect_equal(info$assets_version, SHINYLIVE_ASSETS_VERSION) + + expect_true( + is.list(info$scripts) && + length(info$scripts) == 1 && + nzchar(info$scripts$`codeblock-to-json`) + ) + expect_equal(info$scripts$`codeblock-to-json`, quarto_codeblock_to_json_path()) }) -test_that("quarto_ext handles base-deps", { +test_that("quarto_ext handles `extension base-htmldeps`", { maybe_skip_test() assets_ensure() txt <- collapse(capture.output({ - quarto_ext(c("base-deps", "--sw-dir", "TEST_PATH_SW_DIR")) + quarto_ext(c("extension", "base-htmldeps", "--sw-dir", "TEST_PATH_SW_DIR")) })) items <- jsonlite::parse_json(txt) - worker_items <- Filter(items, f = function(item) { - item$name == "shinylive-serviceworker" - }) + expect_length(items, 2) + + worker_item <- items[[1]] + expect_equal( + worker_item$meta[["shinylive:serviceworker_dir"]], + "TEST_PATH_SW_DIR" + ) + + shinylive_item <- items[[2]] + # Verify there is a quarto html dependency with name `"shinylive"` + expect_equal(shinylive_item$name, "shinylive") + # Verify webr can NOT be found in resources + shinylive_resources <- shinylive_item$resources + expect_false(any(grepl("webr", vapply(shinylive_resources, `[[`, character(1), "name"), fixed = TRUE))) +}) +test_that("quarto_ext handles `extension language-resources`", { + maybe_skip_test() + + assets_ensure() + + txt <- collapse(capture.output({ + quarto_ext(c("extension", "language-resources")) + })) + resources <- jsonlite::parse_json(txt) - expect_length(worker_items, 1) - worker_item <- worker_items[[1]] - expect_equal(worker_item$meta[["shinylive:serviceworker_dir"]], "TEST_PATH_SW_DIR") + # Verify webr folder in path + expect_true(any(grepl("webr", vapply(resources, `[[`, character(1), "name"), fixed = TRUE))) }) -test_that("quarto_ext handles package-deps", { +test_that("quarto_ext handles `extension app-resources`", { maybe_skip_test() assets_ensure() txt <- collapse(capture.output({ - quarto_ext(c("package-deps")) + quarto_ext(c("extension", "app-resources")) })) obj <- jsonlite::parse_json(txt) expect_equal(obj, list())