Skip to content

Commit

Permalink
feat: Customize export template and parameters (#96)
Browse files Browse the repository at this point in the history
* refactor: Use glue to interpolate template parameters

* feat(export): Add `template_dir` parameter

* chore: rlang::use_import_from("rlang", "is_interactive')

* feat(export): Add `template_params` parameter

* fix: Throw an error if template uses a parameter without a value

Otherwise `glue::glue()` will just return an empty string.
This gives a much better and more focused error message.

* fix: prefix is_interactive in examplesIf

* fix: It's better just to emit an empty string for missing template parameters

* tests: Add customized export template test

* feat: switch to whisker (mustache) templating

* feat: copy asset files from `template_dir`

Treat `.html` files as templates for interpolation, and copy all other files as-is.

* fix: includes may include raw HTML

* docs: Add news item

* chore: tweak news

* tests: update tested `export_template/index.html`

* chore: Align template variable names

* chore: Add myself and George as authors

* chore: usethis::use_tidy_description()
  • Loading branch information
gadenbuie authored Jul 1, 2024
1 parent c021a9c commit 49eebff
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 65 deletions.
40 changes: 26 additions & 14 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,40 +1,52 @@
Package: shinylive
Title: Run 'shiny' Applications in the Browser
Version: 0.1.1.9000
Authors@R:
c(
person("Barret", "Schloerke", , "[email protected]", role = c("aut", "cre"),
Authors@R: c(
person("Barret", "Schloerke", , "[email protected]", role = c("aut", "cre"),
comment = c(ORCID = "0000-0001-9986-114X")),
person("Winston", "Chang", role = c("aut"), email = "[email protected]", comment = c(ORCID = "0000-0002-1576-2126")),
person("George", "Stagg", role = "ctb", email = "[email protected]"),
person("Posit Software, PBC", role = c("cph", "fnd"))
)
Description: 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.
person("Winston", "Chang", , "[email protected]", role = "aut",
comment = c(ORCID = "0000-0002-1576-2126")),
person("George", "Stagg", , "[email protected]", role = "aut"),
person("Garrick", "Aden-Buie", , "[email protected]", role = "aut",
comment = c(ORCID = "0000-0002-7111-0077")),
person("Posit Software, PBC", role = c("cph", "fnd"))
)
Description: 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.
License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.1
URL: https://posit-dev.github.io/r-shinylive/,
https://github.com/posit-dev/r-shinylive
BugReports: https://github.com/posit-dev/r-shinylive/issues
URL: https://posit-dev.github.io/r-shinylive/, https://github.com/posit-dev/r-shinylive
Imports:
archive,
brio,
fs,
glue,
gh,
glue,
httr2 (>= 1.0.0),
jsonlite,
pkgdepends,
progress,
rappdirs,
renv,
rlang,
tools
tools,
whisker
Suggests:
httpuv (>= 1.6.12),
pkgcache,
spelling,
testthat (>= 3.0.0)
Config/Needs/website: tidyverse/tidytemplate
Config/testthat/edition: 3
Encoding: UTF-8
Language: en-US
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export(assets_install_link)
export(assets_remove)
export(assets_version)
export(export)
importFrom(rlang,"%||%")
importFrom(rlang,is_interactive)
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

* `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)

* `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)
* `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 `<head>` of the page), and `include_before_body` (HTML added just after `<body>`) and `include_after_body` (HTML added just after `</body>`).
* `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.

# shinylive 0.1.1

* Bump shinylive assets dependency to 0.2.3. (#38)
Expand Down
64 changes: 29 additions & 35 deletions R/app_json.R
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,17 @@ read_app_files <- function(
# """
# Write index.html, edit/index.html, and app.json for an application in the destdir.
# """
#' @importFrom rlang is_interactive
write_app_json <- function(
app_info,
destdir,
html_source_dir,
template_dir,
template_params = list(),
verbose = is_interactive()
) {
verbose_print <- if (isTRUE(verbose)) message else list
stopifnot(inherits(app_info, APP_INFO_CLASS))
# stopifnot(fs::dir_exists(destdir))
stopifnot(fs::dir_exists(html_source_dir))
stopifnot(fs::dir_exists(template_dir))

app_destdir <- fs::path(destdir, app_info$subdir)

Expand All @@ -173,40 +173,34 @@ write_app_json <- function(
subdir_inverse <- paste0(subdir_inverse, "/")
}

# Then iterate over the HTML files in the template directory and interpolate
# the template parameters.
template_files <- fs::dir_ls(template_dir, recurse = TRUE, type = "file")

template_params <- rlang::dots_list(
# Forced parameters
REL_PATH = subdir_inverse,
APP_ENGINE = "r",
# User parameters
!!!template_params,
# Default parameters
title = "Shiny App",
.homonyms = "first"
)

for (copy_info in list(
list(
src = fs::path(html_source_dir, "index.html"),
dest = fs::path(app_destdir, "index.html")
),
list(
src = fs::path(html_source_dir, "edit", "index.html"),
dest = fs::path(app_destdir, "edit", "index.html")
)
)) {
# Create destination directory
fs::dir_create(fs::path_dir(copy_info$dest))

# Read file
index_content <- brio::read_file(copy_info$src)
# Replace template info
index_content <- gsub(
pattern = "{{REL_PATH}}",
replacement = subdir_inverse,
index_content,
fixed = TRUE
)

# Set wasm engine
index_content <- gsub(
pattern = "{{APP_ENGINE}}",
replacement = "r",
index_content,
fixed = TRUE
)
for (template_file in template_files) {
dest_file <- fs::path(app_destdir, fs::path_rel(template_file, template_dir))
fs::dir_create(fs::path_dir(dest_file))

# Save updated file contents
brio::write_file(index_content, copy_info$dest)
if (fs::path_ext(template_file) == "html") {
file_content <- whisker::whisker.render(
template = brio::read_file(template_file),
data = template_params
)
brio::write_file(file_content, dest_file)
} else {
fs::file_copy(template_file, dest_file)
}
}

app_json_output_file <- fs::path(app_destdir, "app.json")
Expand Down
43 changes: 31 additions & 12 deletions R/export.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,38 @@
#' @param destdir Destination directory.
#' @param subdir Subdirectory of `destdir` to write the app to.
#' @param verbose Print verbose output. Defaults to `TRUE` if running
#' interactively.
#' interactively.
#' @param wasm_packages Download and include binary WebAssembly packages as
#' part of the output app's static assets. Defaults to `TRUE`.
#' part of the output app's static assets. Defaults to `TRUE`.
#' @param package_cache Cache downloaded binary WebAssembly packages. Defaults
#' to `TRUE`.
#' to `TRUE`.
#' @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
#' [shinylive asset releases](https://github.com/posit-dev/shinylive/releases)
#' website to learn more information about the available `assets_version` values.
#' exported app. Defaults to [assets_version()]. Note, not all custom assets
#' versions may work with this release of \pkg{shinylive}. Please visit the
#' [shinylive asset releases](https://github.com/posit-dev/shinylive/releases)
#' website to learn more information about the available `assets_version`
#' values.
#' @param template_dir Path to a custom template directory to use when exporting
#' the shinylive app. The template can be copied from the shinylive assets
#' using: `fs::path(shinylive:::assets_dir(), "export_template")`.
#' @param template_params A list of parameters to pass to the template. The
#' supported parameters depends on the template being used. Custom templates
#' may support additional parameters (see `template_dir` for instructions on
#' creating a custom template or to find the current shinylive assets'
#' templates).
#'
#' With shinylive assets > 0.4.1, the default export template supports the
#' following parameters:
#'
#' 1. `title`: The title of the app. Defaults to `"Shiny app"`.
#' 2. `include_in_head`, `include_before_body`, `include_after_body`: Raw
#' HTML to be included in the `<head>`, just after the opening `<body>`,
#' or just before the closing `</body>` tag, respectively.
#' @param ... Ignored
#' @export
#' @return Nothing. The app is exported to `destdir`. Instructions for serving
#' the directory are printed to stdout.
#' @importFrom rlang is_interactive
#' @examplesIf interactive()
#' @examplesIf rlang::is_interactive()
#' app_dir <- system.file("examples", "01_hello", package = "shiny")
#' out_dir <- tempfile("shinylive-export")
#'
Expand All @@ -41,7 +57,9 @@ export <- function(
verbose = is_interactive(),
wasm_packages = TRUE,
package_cache = TRUE,
assets_version = NULL
assets_version = NULL,
template_dir = NULL,
template_params = list()
) {
verbose_print <- if (verbose) message else list
if (is.null(assets_version)) {
Expand Down Expand Up @@ -173,8 +191,9 @@ export <- function(
write_app_json(
app_info,
destdir,
html_source_dir = fs::path(assets_path, "export_template"),
verbose = verbose
template_dir = template_dir %||% fs::path(assets_path, "export_template"),
verbose = verbose,
template_params = template_params
)

verbose_print(
Expand Down
1 change: 0 additions & 1 deletion R/quarto_ext.R
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@
#' ]
#' ```
#'
#' @importFrom rlang is_interactive
quarto_ext <- function(
args = commandArgs(trailingOnly = TRUE),
...,
Expand Down
8 changes: 8 additions & 0 deletions R/shinylive-package.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#' @keywords internal
"_PACKAGE"

## usethis namespace: start
#' @importFrom rlang %||%
#' @importFrom rlang is_interactive
## usethis namespace: end
NULL
28 changes: 25 additions & 3 deletions man/export.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions man/shinylive-package.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions tests/testthat/apps/export_template/edit/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Redirect to editable app</title>
<meta
http-equiv="refresh"
content="0;URL='../index.html?mode=editor-terminal-viewer'"
/>
</head>
<body></body>
</html>
29 changes: 29 additions & 0 deletions tests/testthat/apps/export_template/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{title}}</title>
<meta name="description" content="{{ description }}">
<script
src="./{{REL_PATH}}shinylive/load-shinylive-sw.js"
type="module"
></script>
<script type="module">
import { runExportedApp } from "./{{REL_PATH}}shinylive/shinylive.js";
runExportedApp({
id: "root",
appEngine: "{{APP_ENGINE}}",
relPath: "{{REL_PATH}}",
});
</script>
<link rel="stylesheet" href="./{{REL_PATH}}shinylive/style-resets.css" />
<link rel="stylesheet" href="./{{REL_PATH}}shinylive/shinylive.css" />
{{{ include_in_head }}}
</head>
<body>
{{{ include_before_body }}}
<div style="height: 100vh; width: 100vw" id="root"></div>
{{{ include_after_body }}}
</body>
</html>
Loading

0 comments on commit 49eebff

Please sign in to comment.