|
library(htmltools) |
|
|
|
|
|
# == Top-level function ======================== |
|
|
|
#' @param x An htmltools object that contains HTML dependencies that should be |
|
#' combined. |
|
#' @param combined_first If `TRUE`, the combined JS bundle will come before any |
|
#' JS dependencies that were not able to be bundled (either because they were |
|
#' <script type="module"> or because they are hosted on a CDN). If `FALSE`, |
|
#' then the combined JS bundle will be put at the end. |
|
combine_js <- function(x, combined_first = TRUE) { |
|
rendered <- renderTags(x) |
|
rendered$dependencies <- merge_deps(rendered$dependencies, combined_first) |
|
tagList( |
|
rendered$dependencies, |
|
tags$head(rendered$head), |
|
# TODO: what to do about singletons? |
|
HTML(rendered$html) |
|
) |> browsable(is.browsable(x)) |
|
} |
|
|
|
|
|
# == Supporting functions ======================== |
|
|
|
merge_deps <- function(deps, combined_first = TRUE) { |
|
scripts_and_deps <- lapply(deps, function(dep) { |
|
if (is.null(dep$script)) { |
|
# It has no scripts, do nothing |
|
return(list(scripts = NULL, dep = dep)) |
|
} |
|
if (is.null(dep$src$file)) { |
|
# It's an href-based dependency, do nothing |
|
return(list(scripts = NULL, dep = dep)) |
|
} |
|
split <- split_scripts(dep$script) |
|
dep$script <- split$separate |
|
list( |
|
scripts = split$combine, |
|
dep = dep |
|
) |
|
}) |
|
|
|
scripts_to_combine <- lapply(scripts_and_deps, function(sd) { |
|
dep <- sd$dep |
|
scripts <- sd$scripts |
|
script_paths <- file.path(dep$src$file, scripts) |
|
if (!is.null(sd$package)) { |
|
script_paths <- htmltools:::system_file(script_paths, package = sd$package) |
|
} |
|
# TODO: Assert that the script files all exist |
|
return(script_paths) |
|
}) |> unlist(recursive = FALSE) |
|
|
|
modified_dependencies <- lapply(scripts_and_deps, function(sd) sd$dep) |
|
|
|
destdir <- tempfile(pattern = "combined_js_dep") |
|
dir.create(destdir) |
|
destfile <- file.path(destdir, "combined.js") |
|
f <- file(destfile, "wt") |
|
on.exit(close(f)) |
|
for (script_path in scripts_to_combine) { |
|
writeLines(paste("/***", script_path, "***/\n"), f) |
|
writeLines(readLines(script_path, encoding = "utf-8", warn = FALSE), f) |
|
writeLines("\n\n", f) |
|
} |
|
|
|
combined_dep <- htmlDependency( |
|
"htmltools-combined-js", |
|
"1.0.0", |
|
src = destdir, |
|
script = "combined.js", |
|
all_files = FALSE |
|
) |
|
|
|
if (isTRUE(combined_first)) { |
|
c(list(combined_dep), modified_dependencies) |
|
} else { |
|
c(modified_dependencies, list(combined_dep)) |
|
} |
|
} |
|
|
|
split_scripts <- function(scripts) { |
|
if (is.null(scripts)) { |
|
return(list(separate=NULL, combine=NULL)) |
|
} else if (is.character(scripts)) { |
|
return(list(separate=NULL, combine=scripts)) |
|
} else if (is.list(scripts)) { |
|
can_combine <- vapply(scripts, function(script) { |
|
if (is.character(script)) { |
|
TRUE |
|
} else if (is.list(script)){ |
|
!identical(script$type, "module") |
|
} else { |
|
stop("unexpected script shape") |
|
} |
|
}, logical(1)) |
|
return(list( |
|
separate = scripts[!can_combine], |
|
combine = normalize_scripts(scripts[can_combine]) |
|
)) |
|
} else { |
|
stop("unexpected scripts shape") |
|
} |
|
} |
|
|
|
normalize_scripts <- function(scripts) { |
|
lapply(scripts, function(script) { |
|
if (is.character(script)) { |
|
script |
|
} else if (is.list(script)) { |
|
script$src |
|
} else { |
|
stop("unexpected script shape") |
|
} |
|
}) |
|
} |
|
|
|
|
|
# == Example ======================== |
|
|
|
library(ggplot2) |
|
library(plotly) |
|
library(leaflet) |
|
library(bslib) |
|
|
|
x <- page_fillable( |
|
ggplotly(ggplot(cars, aes(speed, dist)) + geom_point()), |
|
leaflet(quakes) |> addTiles() |> addCircleMarkers() |
|
) |
|
print(combine_js(x), browse=TRUE) |