diff --git a/R/export.R b/R/export.R index 173beeb..660791c 100644 --- a/R/export.R +++ b/R/export.R @@ -14,6 +14,10 @@ #' part of the output app's static assets. Defaults to `TRUE`. #' @param package_cache Cache downloaded binary WebAssembly packages. Defaults #' to `TRUE`. +#' @param max_filesize Maximum file size for bundling of WebAssembly package +#' assets. Parsed by [fs::fs_bytes()]. Defaults to `"100M"`. The default +#' value can be changed by setting the environment variable +#' `SHINYLIVE_DEFAULT_MAX_FILESIZE`. Set to `Inf`, `NA` or `-1` to disable. #' @param assets_version The version of the Shinylive assets to use in the #' exported app. Defaults to [assets_version()]. Note, not all custom assets #' versions may work with this release of \pkg{shinylive}. Please visit the @@ -59,6 +63,7 @@ export <- function( quiet = getOption("shinylive.quiet", !is_interactive()), wasm_packages = TRUE, package_cache = TRUE, + max_filesize = NULL, assets_version = NULL, template_dir = NULL, template_params = list(), @@ -194,7 +199,7 @@ export <- function( # Copy app package dependencies as Wasm binaries # ========================================================================= if (wasm_packages) { - download_wasm_packages(appdir, destdir, package_cache) + download_wasm_packages(appdir, destdir, package_cache, max_filesize) } # ========================================================================= diff --git a/R/packages.R b/R/packages.R index 5b42849..4989aa9 100644 --- a/R/packages.R +++ b/R/packages.R @@ -1,3 +1,11 @@ +SHINYLIVE_DEFAULT_MAX_FILESIZE <- "100MB" + +# Sys env maximum filesize for asset bundling +sys_env_max_filesize <- function() { + max_fs_env <- Sys.getenv("SHINYLIVE_DEFAULT_MAX_FILESIZE") + if (max_fs_env == "") NULL else max_fs_env +} + # Resolve package list hard dependencies resolve_dependencies <- function(pkgs, local = TRUE) { pkg_refs <- if (local) { @@ -178,7 +186,22 @@ env_download_wasm_core_packages <- function() { strsplit(pkgs, "\\s*[ ,\n]\\s*")[[1]] } -download_wasm_packages <- function(appdir, destdir, package_cache) { +download_wasm_packages <- function(appdir, destdir, package_cache, max_filesize) { + max_filesize_missing <- is.null(sys_env_max_filesize()) && is.null(max_filesize) + max_filesize_cli_fn <- if (max_filesize_missing) cli::cli_warn else cli::cli_abort + + max_filesize <- max_filesize %||% sys_env_max_filesize() %||% SHINYLIVE_DEFAULT_MAX_FILESIZE + max_filesize <- if (is.na(max_filesize) || (max_filesize < 0)) Inf else max_filesize + max_filesize_val <- max_filesize + max_filesize <- fs::fs_bytes(max_filesize) + if (is.na(max_filesize)) { + cli::cli_warn(c( + "!" = "Could not parse `max_filesize` value: {.code {max_filesize_val}}", + "i" = "Setting to {.code {SHINYLIVE_DEFAULT_MAX_FILESIZE}}" + )) + max_filesize <- fs::fs_bytes(SHINYLIVE_DEFAULT_MAX_FILESIZE) + } + # Core packages in base webR image that we don't need to download shiny_pkgs <- c("shiny", "bslib", "renv") shiny_pkgs <- resolve_dependencies(shiny_pkgs, local = FALSE) @@ -240,13 +263,31 @@ download_wasm_packages <- function(appdir, destdir, package_cache) { # Create package ref and lookup download URLs meta <- prepare_wasm_metadata(pkg, prev_meta) - if (!meta$cached && length(meta$assets) > 0) { + if (!meta$cached) { # Download Wasm binaries and copy to static assets dir for (file in meta$assets) { - utils::download.file(file$url, fs::path(pkg_subdir, file$filename)) + path <- fs::path(pkg_subdir, file$filename) + utils::download.file(file$url, path) + + # Disallow this package if an asset is too large + if (fs::file_size(path) > max_filesize) { + fs::dir_delete(pkg_subdir) + meta$assets = list() + max_filesize_cli_fn(c( + "!" = "The file size of package {.pkg {pkg}} is larger than the maximum allowed file size of {.strong {max_filesize}}.", + "!" = "This package will not be included as part of the WebAssembly asset bundle.", + "i" = "Set the maximum allowed size to {.code -1}, {.code Inf}, or {.code NA} to disable this check.", + "i" = if (max_filesize_missing) "Explicitly set the maximum allowed size to treat this as an error." else NULL + )) + break + } } + meta$cached <- TRUE - meta$path <- glue::glue("packages/{pkg}/{meta$assets[[1]]$filename}") + meta$path <- NULL + if (length(meta$assets) > 0) { + meta$path <- glue::glue("packages/{pkg}/{meta$assets[[1]]$filename}") + } } meta }) diff --git a/R/quarto_ext.R b/R/quarto_ext.R index c5b9229..1034de6 100644 --- a/R/quarto_ext.R +++ b/R/quarto_ext.R @@ -256,7 +256,7 @@ build_app_resources <- function(app_json) { # Download wasm binaries ready to embed into Quarto deps withr::with_options( list(shinylive.quiet = TRUE), - download_wasm_packages(appdir, destdir, package_cache = TRUE) + download_wasm_packages(appdir, destdir, package_cache = TRUE, max_filesize = NULL) ) # Enumerate R package Wasm binaries and prepare the VFS images as html deps diff --git a/man/export.Rd b/man/export.Rd index 3d8e633..4cb41d3 100644 --- a/man/export.Rd +++ b/man/export.Rd @@ -12,6 +12,7 @@ export( quiet = getOption("shinylive.quiet", !is_interactive()), wasm_packages = TRUE, package_cache = TRUE, + max_filesize = NULL, assets_version = NULL, template_dir = NULL, template_params = list(), @@ -37,6 +38,11 @@ part of the output app's static assets. Defaults to \code{TRUE}.} \item{package_cache}{Cache downloaded binary WebAssembly packages. Defaults to \code{TRUE}.} +\item{max_filesize}{Maximum file size for bundling of WebAssembly package +assets. Parsed by \code{\link[fs:fs_bytes]{fs::fs_bytes()}}. Defaults to \code{"100M"}. The default +value can be changed by setting the environment variable +\code{SHINYLIVE_DEFAULT_MAX_FILESIZE}. Set to \code{Inf}, \code{NA} or \code{-1} to disable.} + \item{assets_version}{The version of the Shinylive assets to use in the exported app. Defaults to \code{\link[=assets_version]{assets_version()}}. Note, not all custom assets versions may work with this release of \pkg{shinylive}. Please visit the diff --git a/tests/testthat/apps/app-utf8/app.R b/tests/testthat/apps/app-utf8/app.R new file mode 100644 index 0000000..b21dfdf --- /dev/null +++ b/tests/testthat/apps/app-utf8/app.R @@ -0,0 +1,7 @@ +library(shiny) +library(utf8) + +ui <- fluidPage() +server <- function(input, output) {} + +shinyApp(ui, server) diff --git a/tests/testthat/test-export.R b/tests/testthat/test-export.R index 0a306f1..cfc9b68 100644 --- a/tests/testthat/test-export.R +++ b/tests/testthat/test-export.R @@ -134,4 +134,50 @@ test_that("export with template", { index_content, "" ) -}) \ No newline at end of file +}) + +test_that("export - include R package in wasm assets", { + maybe_skip_test() + + assets_ensure() + + # Ensure pkgcache metadata has been loaded + invisible(pkgcache::meta_cache_list()) + + # Create a temporary output directory + out_dir <- file.path(tempfile(), "out") + pkg_dir <- file.path(out_dir, "shinylive", "webr", "packages") + + # A package with an external dependency + app_dir <- test_path("apps", "app-utf8") + asset_package <- c("utf8") + + # Default filesize 100MB + expect_silent_unattended({ + export(app_dir, out_dir) + }) + expect_contains(dir(pkg_dir), c(asset_package)) + unlink_path(out_dir) + + # No maximum filesize + expect_silent_unattended({ + export(app_dir, out_dir, max_filesize = Inf) + }) + expect_contains(dir(pkg_dir), c(asset_package)) + unlink_path(out_dir) + + # Set a maximum filesize + expect_error({ + export(app_dir, out_dir, max_filesize = "1K") + }) + unlink_path(out_dir) + + expect_error({ + withr::with_envvar( + list("SHINYLIVE_DEFAULT_MAX_FILESIZE" = "1K"), + export(app_dir, out_dir) + ) + }) + unlink_path(out_dir) + +})