From 0d7c2b12b09b9dd03fe8c35659e90d178b273732 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 1 Nov 2019 12:56:48 -0400 Subject: [PATCH 01/28] add eager_loading parameter --- R/dash.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/R/dash.R b/R/dash.R index 6eba02d9..20e0a851 100644 --- a/R/dash.R +++ b/R/dash.R @@ -9,6 +9,7 @@ #' server = fiery::Fire$new(), #' assets_folder = 'assets', #' assets_url_path = '/assets', +#' eager_loading = FALSE, #' assets_ignore = '', #' serve_locally = TRUE, #' routes_pathname_prefix = '/', @@ -26,6 +27,7 @@ #' .css files will be loaded immediately unless excluded by `assets_ignore`, #' and other files such as images will be served if requested. Default is `assets`. \cr #' `assets_url_path` \tab \tab Character. Specify the URL path for asset serving. Default is `assets`. \cr +#' `eager_loading` \tab \tab Logical. Controls whether asynchronous resources are prefetched (if `TRUE`) or loaded on-demand (if `FALSE`). \cr #' `assets_ignore` \tab \tab Character. A regular expression, to match assets to omit from #' immediate loading. Ignored files will still be served if specifically requested. You #' cannot use this to prevent access to sensitive files. \cr @@ -136,6 +138,7 @@ Dash <- R6::R6Class( server = fiery::Fire$new(), assets_folder = 'assets', assets_url_path = '/assets', + eager_loading = FALSE, assets_ignore = '', serve_locally = TRUE, routes_pathname_prefix = NULL, From ef59eeb9874a37f9506d1e3013901f8b783bb96e Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 1 Nov 2019 12:59:48 -0400 Subject: [PATCH 02/28] :pencil2: add fields for eager_loading --- R/dash.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/dash.R b/R/dash.R index 20e0a851..bce63532 100644 --- a/R/dash.R +++ b/R/dash.R @@ -157,6 +157,7 @@ Dash <- R6::R6Class( # save relevant args as private fields private$name <- name private$serve_locally <- serve_locally + private$eager_loading <- eager_loading # remove leading and trailing slash(es) if present private$assets_folder <- gsub("^/+|/+$", "", assets_folder) # remove trailing slash in assets_url_path, if present @@ -610,6 +611,7 @@ Dash <- R6::R6Class( # private fields defined on initiation name = NULL, serve_locally = NULL, + eager_loading = NULL, assets_folder = NULL, assets_url_path = NULL, assets_ignore = NULL, From f36c77a8b02a70c01c697dc16d5880981d007093 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 7 Nov 2019 00:05:41 -0500 Subject: [PATCH 03/28] :name_badge: add buildFingerprint --- R/utils.R | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/R/utils.R b/R/utils.R index c69816aa..21a3894e 100644 --- a/R/utils.R +++ b/R/utils.R @@ -923,3 +923,15 @@ getIdProps <- function(output) { clientsideFunction <- function(namespace, function_name) { return(list(namespace=namespace, function_name=function_name)) } + +buildFingerprint <- function(path, version, hash_value) { + path <- file.path(path) + filename <- tools::file_path_sans_ext(basename(path)) + extension <- tools::file_ext(path) + + sprintf("%s.v%sm%s.%s", + file.path(dirname(mypath), filename), + gsub("[^\\w-]", "_", version, perl = TRUE), + hash_value, + extension) +} From a869c4ff562e18c44742cc0f19af12da0b5d2a8a Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 7 Nov 2019 14:18:48 -0500 Subject: [PATCH 04/28] :name_badge: add checkFingerprint --- R/utils.R | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/R/utils.R b/R/utils.R index 21a3894e..820779af 100644 --- a/R/utils.R +++ b/R/utils.R @@ -935,3 +935,13 @@ buildFingerprint <- function(path, version, hash_value) { hash_value, extension) } + +checkFingerprint <- function(path) { + name_parts <- unlist(strsplit(basename(path), ".", fixed = TRUE)) + + # Check if the resource has a fingerprint + if ((length(name_parts) > 2) && grepl("^v[\\w-]+m[0-9a-fA-F]+$", name_parts[2], perl = TRUE)) { + return(list(paste(name_parts[name_parts != name_parts[2]], collapse = "."), TRUE)) + } + return(list(path, FALSE)) +} From 46157f7e427e44be10a6844c1eec753440651922 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 3 Dec 2019 12:20:21 -0500 Subject: [PATCH 05/28] temporarily disable percy --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0501af6f..e9eaaa97 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,6 +7,8 @@ jobs: working_directory: ~/dashr docker: - image: byronz/dashr:ci + environment: + PERCY_ENABLE: 0 steps: - checkout From bd3ae5ecaa044c94c3b55d0a70cd99718bc083fd Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 4 Dec 2019 15:03:16 -0500 Subject: [PATCH 06/28] :see_no_evil: use path instead of mypath --- R/utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index 820779af..a2057cab 100644 --- a/R/utils.R +++ b/R/utils.R @@ -930,7 +930,7 @@ buildFingerprint <- function(path, version, hash_value) { extension <- tools::file_ext(path) sprintf("%s.v%sm%s.%s", - file.path(dirname(mypath), filename), + file.path(dirname(path), filename), gsub("[^\\w-]", "_", version, perl = TRUE), hash_value, extension) From 1bf6158185ccf490c1d944036100290619d5d86b Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 5 Dec 2019 15:34:23 -0500 Subject: [PATCH 07/28] :truck: dep path resolution into fn for :camel: --- R/utils.R | 60 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/R/utils.R b/R/utils.R index a2057cab..29de155d 100644 --- a/R/utils.R +++ b/R/utils.R @@ -155,31 +155,7 @@ render_dependencies <- function(dependencies, local = TRUE, prefix=NULL) { # package and add the version number of the package as a query # parameter for cache busting if (!is.null(dep$package)) { - if(!(is.null(dep$script))) { - filename <- dep$script - } else { - filename <- dep$stylesheet - } - - dep_path <- paste(dep$src$file, filename, sep="/") - - # the gsub line is to remove stray duplicate slashes, to - # permit exact string matching on pathnames - dep_path <- gsub("//+", - "/", - dep_path) - - full_path <- system.file(dep_path, - package = dep$package) - - if (!file.exists(full_path)) { - warning(sprintf("The dependency path '%s' within the '%s' package is invalid; cannot find '%s'.", - full_path, - dep$package, - filename), - call. = FALSE) - } - + full_path <- getDependencyPath(dep) modified <- as.integer(file.mtime(full_path)) } else { modified <- as.integer(Sys.time()) @@ -945,3 +921,37 @@ checkFingerprint <- function(path) { } return(list(path, FALSE)) } + +getDependencyPath <- function(dep) { + if (missing(dep)) { + stop("getDependencyPath requires that a valid dependency object is passed. Please verify that dep is non-missing.") + } + + if(!(is.null(dep$script))) { + filename <- dep$script + } else { + filename <- dep$stylesheet + } + + dep_path <- paste(dep$src$file, filename, sep="/") + + # the gsub line is to remove stray duplicate slashes, to + # permit exact string matching on pathnames + dep_path <- gsub("//+", + "/", + dep_path) + + full_path_to_dependency <- system.file(dep_path, + package = dep$package) + + if (!file.exists(full_path_to_dependency)) { + warning(sprintf("The dependency path '%s' within the '%s' package is invalid; cannot find '%s'.", + full_path_to_dependency, + dep$package, + filename), + call. = FALSE) + } + + return(full_path_to_dependency) +} + From fa4c4bdf6459adc553ed7c9883ee52692aa1e6b5 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 5 Dec 2019 15:35:04 -0500 Subject: [PATCH 08/28] :pencil2: Dash-friendly ext/filename helpers --- R/utils.R | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/R/utils.R b/R/utils.R index 29de155d..54ef81b1 100644 --- a/R/utils.R +++ b/R/utils.R @@ -902,8 +902,8 @@ clientsideFunction <- function(namespace, function_name) { buildFingerprint <- function(path, version, hash_value) { path <- file.path(path) - filename <- tools::file_path_sans_ext(basename(path)) - extension <- tools::file_ext(path) + filename <- getFileSansExt(path) + extension <- getFileExt(path) sprintf("%s.v%sm%s.%s", file.path(dirname(path), filename), @@ -955,3 +955,17 @@ getDependencyPath <- function(dep) { return(full_path_to_dependency) } +# the base R functions which strip extensions and filenames without +# extensions from paths are not robust to multipart extensions, +# such as .js.map or .min.js; these are functions intended to +# perform reliably in such cases. the first occurrence of a dot +# is replaced with an asterisk, which is generally an invalid +# filename character in any modern filesystem, since it represents +# a wildcard. the resulting string is then split on the asterisk. +getFileSansExt <- function(filepath) { + unlist(strsplit(sub("[.]", "*", basename(filepath)), "*"))[1] +} + +getFileExt <- function(filepath) { + unlist(strsplit(sub("[.]", "*", basename(filepath)), "*"))[2] +} From f351b8b454d0112c0a2f7e8302fd66f6c0b6fce5 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 5 Dec 2019 15:36:27 -0500 Subject: [PATCH 09/28] use getDependencyPath, + :paw_prints:/Etag support --- R/dash.R | 56 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/R/dash.R b/R/dash.R index bce63532..a21afb5c 100644 --- a/R/dash.R +++ b/R/dash.R @@ -373,12 +373,25 @@ Dash <- R6::R6Class( # This endpoint supports dynamic dependency loading # during `_dash-update-component` -- for reference: - # https://github.com/plotly/dash/blob/1249ffbd051bfb5fdbe439612cbec7fa8fff5ab5/dash/dash.py#L488 # https://docs.python.org/3/library/pkgutil.html#pkgutil.get_data + # + # analogous to + # https://github.com/plotly/dash/blob/2d735aa250fc67b14dc8f6a337d15a16b7cbd6f8/dash/dash.py#L543-L551 dash_suite <- paste0(self$config$routes_pathname_prefix, "_dash-component-suites/:package_name/:filename") - + route$add_handler("get", dash_suite, function(request, response, keys, ...) { filename <- basename(file.path(keys$filename)) + + # checkFingerprint returns a list of length 2, the first element is + # the un-fingerprinted path, if a fingerprint is present (otherwise + # the original path is returned), while the second element indicates + # whether the original filename included a valid fingerprint (by + # Dash convention) + fingerprinting_metadata <- checkFingerprint(filename) + + filename <- fingerprinting_metadata[[1]] + has_fingerprint <- fingerprinting_metadata[[2]] == TRUE + dep_list <- c(private$dependencies_internal, private$dependencies, private$dependencies_user) @@ -388,7 +401,6 @@ Dash <- R6::R6Class( clean_dependencies(dep_list) ) - # return warning if a dependency goes unmatched, since the page # will probably fail to render properly anyway without it if (length(dep_pkg$rpkg_path) == 0) { @@ -399,17 +411,36 @@ Dash <- R6::R6Class( response$body <- NULL response$status <- 404L } else { + # need to check for debug mode, don't cache, don't etag + # if debug mode is not active dep_path <- system.file(dep_pkg$rpkg_path, package = dep_pkg$rpkg_name) response$body <- readLines(dep_path, warn = FALSE, encoding = "UTF-8") - response$status <- 200L - response$set_header('Cache-Control', - sprintf('public, max-age=%s', - components_cache_max_age) - ) + + if (!private$debug && has_fingerprint) { + response$status <- 200L + response$set_header('Cache-Control', + sprintf('public, max-age=%s', + 31536000) # 1 year + ) + } else if (!private$debug && !has_fingerprint) { + modified <- as.character(as.integer(file.mtime(dep_path))) + + response$set_header('ETag', modified) + + request_etag <- request$headers[["If-None-Match"]] + + if (modified == request_etag) { + response$body <- NULL + response$status <- 304L + } + } else { + response$status <- 200L + } + response$type <- get_mimetype(filename) } @@ -888,7 +919,14 @@ Dash <- R6::R6Class( scripts_deps <- compact(lapply(depsAll, function(dep) { if (is.null(dep$script)) return(NULL) dep$stylesheet <- NULL - dep + # need to fingerprint the script name + # first, identify modtime of file + # then create a modifictation timestamp as integer + # finally, invoke buildFingerprint to generate a + # compliant fingerprinted path for the renderer + script_mtime <- file.mtime(getDependencyPath(dep)) + modtime <- as.integer(script_mtime) + dep$script <- buildFingerprint(dep$script, dep$version, modtime) })) scripts_deps <- render_dependencies(scripts_deps, From a4e6754647e88041b2e65a3a2e942c929c8bc1c2 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 13 Dec 2019 15:26:12 -0500 Subject: [PATCH 10/28] updates to support async --- DESCRIPTION | 10 +++++----- R/dash.R | 32 ++++++++++++++++++++++--------- R/utils.R | 54 +++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 68 insertions(+), 28 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9a7f5c78..180c0f2b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,19 +1,19 @@ Package: dash Title: An Interface to the Dash Ecosystem for Authoring Reactive Web Applications -Version: 0.1.0 +Version: 0.2.0 Authors@R: c(person("Chris", "Parmer", role = c("aut"), email = "chris@plot.ly"), person("Ryan Patrick", "Kyle", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-5829-9867"), email = "ryan@plot.ly"), person("Carson", "Sievert", role = c("aut"), comment = c(ORCID = "0000-0002-4958-2844")), person(family = "Plotly Technologies", role = "cph")) Description: A framework for building analytical web applications, Dash offers a pleasant and productive development experience. No JavaScript required. Depends: R (>= 3.0.2) Imports: - dashHtmlComponents (== 1.0.0), - dashCoreComponents (== 1.0.0), - dashTable (== 4.0.2), + dashHtmlComponents (== 1.0.2), + dashCoreComponents (== 1.6.0), + dashTable (== 4.5.1), R6, fiery (> 1.0.0), routr (> 0.2.0), plotly, - reqres, + reqres (>= 0.2.3), jsonlite, htmltools, assertthat, diff --git a/R/dash.R b/R/dash.R index a21afb5c..8fcce7a2 100644 --- a/R/dash.R +++ b/R/dash.R @@ -41,10 +41,7 @@ #' `external_stylesheets` \tab \tab An optional list of valid URLs from which #' to serve CSS for rendered pages.\cr #' `suppress_callback_exceptions` \tab \tab Whether to relay warnings about -#' possible layout mis-specifications when registering a callback. \cr -#' `components_cache_max_age` \tab \tab An integer value specifying the time -#' interval prior to expiring cached assets. The default is 2678400 seconds, -#' or 31 calendar days. +#' possible layout mis-specifications when registering a callback. #' } #' #' @section Fields: @@ -145,8 +142,7 @@ Dash <- R6::R6Class( requests_pathname_prefix = NULL, external_scripts = NULL, external_stylesheets = NULL, - suppress_callback_exceptions = FALSE, - components_cache_max_age = 2678400) { + suppress_callback_exceptions = FALSE) { # argument type checking assertthat::assert_that(is.character(name)) @@ -517,7 +513,7 @@ Dash <- R6::R6Class( response$set_header('Cache-Control', sprintf('public, max-age=%s', - components_cache_max_age) + '31536000') ) response$type <- 'image/x-icon' response$status <- 200L @@ -913,7 +909,21 @@ Dash <- R6::R6Class( css_deps <- render_dependencies(css_deps, local = private$serve_locally, prefix=self$config$requests_pathname_prefix) - + + # ensure that no dependency has both async and dynamic set + if (any(vapply(foo, function(dep) { + length(intersect(c("dynamic", "async"), + names(dep))) > 1, + logical(1))) + } stop("Can't have both 'dynamic' and 'async' in a Dash dependency; please correct and reload.", call. = FALSE) + + # remove dependencies which are dynamic from the script list + # to avoid placing them into the index + depsAll <- depsAll[!vapply(depsAll, + isDynamic, + logical(1), + eager_loading = private$eager_loading)] + # scripts go after dash-renderer dependencies (i.e., React), # but before dash-renderer itself scripts_deps <- compact(lapply(depsAll, function(dep) { @@ -924,9 +934,13 @@ Dash <- R6::R6Class( # then create a modifictation timestamp as integer # finally, invoke buildFingerprint to generate a # compliant fingerprinted path for the renderer + # + # need to move this block out so we don't set + # dep$script script_mtime <- file.mtime(getDependencyPath(dep)) modtime <- as.integer(script_mtime) dep$script <- buildFingerprint(dep$script, dep$version, modtime) + dep })) scripts_deps <- render_dependencies(scripts_deps, @@ -991,7 +1005,7 @@ Dash <- R6::R6Class( scripts_assets, scripts_invoke_renderer), collapse = "\n") - + return(list(css_tags = css_tags, scripts_tags = scripts_tags, favicon = favicon)) diff --git a/R/utils.R b/R/utils.R index 54ef81b1..990933c6 100644 --- a/R/utils.R +++ b/R/utils.R @@ -167,7 +167,6 @@ render_dependencies <- function(dependencies, local = TRUE, prefix=NULL) { if ("script" %in% names(dep) && tools::file_ext(dep[["script"]]) != "map") { if (!(is_local) & !(is.null(dep$src$href))) { html <- generate_js_dist_html(href = dep$src$href) - } else { dep[["script"]] <- paste0(path_prefix, "_dash-component-suites/", @@ -919,7 +918,7 @@ checkFingerprint <- function(path) { if ((length(name_parts) > 2) && grepl("^v[\\w-]+m[0-9a-fA-F]+$", name_parts[2], perl = TRUE)) { return(list(paste(name_parts[name_parts != name_parts[2]], collapse = "."), TRUE)) } - return(list(path, FALSE)) + return(list(basename(path), FALSE)) } getDependencyPath <- function(dep) { @@ -928,12 +927,12 @@ getDependencyPath <- function(dep) { } if(!(is.null(dep$script))) { - filename <- dep$script + filename <- checkFingerprint(dep$script)[[1]] } else { filename <- dep$stylesheet } - dep_path <- paste(dep$src$file, filename, sep="/") + dep_path <- file.path(dep$src$file, filename) # the gsub line is to remove stray duplicate slashes, to # permit exact string matching on pathnames @@ -941,15 +940,20 @@ getDependencyPath <- function(dep) { "/", dep_path) - full_path_to_dependency <- system.file(dep_path, - package = dep$package) - + # this may generate doubled slashes, which should not + # pose problems on Mac OS, Windows, or Linux systems + full_path_to_dependency <- system.file(file.path(dep$src$file, + returnDirname(dep$script), + filename), + package=dep$package) + if (!file.exists(full_path_to_dependency)) { - warning(sprintf("The dependency path '%s' within the '%s' package is invalid; cannot find '%s'.", - full_path_to_dependency, - dep$package, - filename), - call. = FALSE) + write(crayon::yellow(sprintf("The dependency path '%s' within the '%s' package is invalid; cannot find '%s'.", + dep_path, + dep$package, + filename) + ), + stderr()) } return(full_path_to_dependency) @@ -963,9 +967,31 @@ getDependencyPath <- function(dep) { # filename character in any modern filesystem, since it represents # a wildcard. the resulting string is then split on the asterisk. getFileSansExt <- function(filepath) { - unlist(strsplit(sub("[.]", "*", basename(filepath)), "*"))[1] + unlist(strsplit(sub("[.]", "*", basename(filepath)), "[*]"))[1] } getFileExt <- function(filepath) { - unlist(strsplit(sub("[.]", "*", basename(filepath)), "*"))[2] + unlist(strsplit(sub("[.]", "*", basename(filepath)), "[*]"))[2] +} + +returnDirname <- function(filepath) { + dirname <- dirname(filepath) + if (dirname == ".") + return("") + return(dirname) +} + +isDynamic <- function(eager_loading, resource) { + if ( + is.null(resource$dynamic) && is.null(resource$async) + ) + return(FALSE) + # need assert that async and dynamic are not both present + if ( + (!is.null(resource$dynamic) && (resource$dynamic == FALSE)) || + (eager_loading==TRUE && !is.null(resource$async) && (resource$async %in% c("eager", TRUE))) + ) + return(FALSE) + else + return(TRUE) } From bae3256979491fc5343d72867ce2b3d137040766 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 14 Dec 2019 22:36:31 -0500 Subject: [PATCH 11/28] :see_no_evil: fix misplaced paren --- R/dash.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/dash.R b/R/dash.R index 279770bb..58168725 100644 --- a/R/dash.R +++ b/R/dash.R @@ -1235,7 +1235,7 @@ Dash <- R6::R6Class( # ensure that no dependency has both async and dynamic set if (any(vapply(foo, function(dep) { length(intersect(c("dynamic", "async"), - names(dep))) > 1, + names(dep)) > 1), logical(1))) } stop("Can't have both 'dynamic' and 'async' in a Dash dependency; please correct and reload.", call. = FALSE) From c04b8b4a89589ea0e39f5baefb356087b38b996e Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 14 Dec 2019 23:18:45 -0500 Subject: [PATCH 12/28] fix variable name and parens --- R/dash.R | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/R/dash.R b/R/dash.R index 58168725..03baa4de 100644 --- a/R/dash.R +++ b/R/dash.R @@ -1233,11 +1233,14 @@ Dash <- R6::R6Class( prefix=self$config$requests_pathname_prefix) # ensure that no dependency has both async and dynamic set - if (any(vapply(foo, function(dep) { - length(intersect(c("dynamic", "async"), - names(dep)) > 1), - logical(1))) - } stop("Can't have both 'dynamic' and 'async' in a Dash dependency; please correct and reload.", call. = FALSE) + if (any( + vapply(depsAll, function(dep) + length(intersect(c("dynamic", "async"), names(dep))) > 1, + logical(1) + ) + ) + ) + stop("Can't have both 'dynamic' and 'async' in a Dash dependency; please correct and reload.", call. = FALSE) # remove dependencies which are dynamic from the script list # to avoid placing them into the index From fd651869eaf0db695e15c5b041b5a98cafc56782 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 14 Dec 2019 22:36:31 -0500 Subject: [PATCH 13/28] :see_no_evil: fix misplaced paren fix variable name and parens --- R/dash.R | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/R/dash.R b/R/dash.R index 279770bb..03baa4de 100644 --- a/R/dash.R +++ b/R/dash.R @@ -1233,11 +1233,14 @@ Dash <- R6::R6Class( prefix=self$config$requests_pathname_prefix) # ensure that no dependency has both async and dynamic set - if (any(vapply(foo, function(dep) { - length(intersect(c("dynamic", "async"), - names(dep))) > 1, - logical(1))) - } stop("Can't have both 'dynamic' and 'async' in a Dash dependency; please correct and reload.", call. = FALSE) + if (any( + vapply(depsAll, function(dep) + length(intersect(c("dynamic", "async"), names(dep))) > 1, + logical(1) + ) + ) + ) + stop("Can't have both 'dynamic' and 'async' in a Dash dependency; please correct and reload.", call. = FALSE) # remove dependencies which are dynamic from the script list # to avoid placing them into the index From ac81b373701ac936512ff8bff7300123da852a9d Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 16 Dec 2019 22:51:21 -0500 Subject: [PATCH 14/28] :hammer: refactor tag generation --- R/dash.R | 11 ----------- R/utils.R | 3 +++ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/R/dash.R b/R/dash.R index 03baa4de..fa8d6090 100644 --- a/R/dash.R +++ b/R/dash.R @@ -1254,17 +1254,6 @@ Dash <- R6::R6Class( scripts_deps <- compact(lapply(depsAll, function(dep) { if (is.null(dep$script)) return(NULL) dep$stylesheet <- NULL - # need to fingerprint the script name - # first, identify modtime of file - # then create a modifictation timestamp as integer - # finally, invoke buildFingerprint to generate a - # compliant fingerprinted path for the renderer - # - # need to move this block out so we don't set - # dep$script - script_mtime <- file.mtime(getDependencyPath(dep)) - modtime <- as.integer(script_mtime) - dep$script <- buildFingerprint(dep$script, dep$version, modtime) dep })) diff --git a/R/utils.R b/R/utils.R index 3ba1f849..2cad0373 100644 --- a/R/utils.R +++ b/R/utils.R @@ -168,6 +168,9 @@ render_dependencies <- function(dependencies, local = TRUE, prefix=NULL) { if (!(is_local) & !(is.null(dep$src$href))) { html <- generate_js_dist_html(href = dep$src$href) } else { + script_mtime <- file.mtime(getDependencyPath(dep)) + modtime <- as.integer(script_mtime) + dep$script <- buildFingerprint(dep$script, dep$version, modtime) dep[["script"]] <- paste0(path_prefix, "_dash-component-suites/", dep$name, From 0c77d328306746da8e441a14909762e7a2210e7f Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 16 Dec 2019 22:52:14 -0500 Subject: [PATCH 15/28] :sparkles: properly support gz compression --- R/dash.R | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/R/dash.R b/R/dash.R index fa8d6090..dbe16735 100644 --- a/R/dash.R +++ b/R/dash.R @@ -170,6 +170,7 @@ Dash <- R6::R6Class( requests_pathname_prefix = NULL, external_scripts = NULL, external_stylesheets = NULL, + compress = TRUE, suppress_callback_exceptions = FALSE) { # argument type checking @@ -188,6 +189,7 @@ Dash <- R6::R6Class( private$assets_url_path <- sub("/$", "", assets_url_path) private$assets_ignore <- assets_ignore private$suppress_callback_exceptions <- suppress_callback_exceptions + private$compress <- compress private$app_root_path <- getAppPath() private$app_launchtime <- as.integer(Sys.time()) private$meta_tags <- meta_tags @@ -244,6 +246,9 @@ Dash <- R6::R6Class( response$body <- to_JSON(lay, pretty = TRUE) response$status <- 200L response$type <- 'json' + + if (private$compress) + response$compress() TRUE }) @@ -254,6 +259,7 @@ Dash <- R6::R6Class( response$body <- to_JSON(list()) response$status <- 200L response$type <- 'json' + return(FALSE) } @@ -269,6 +275,8 @@ Dash <- R6::R6Class( response$body <- to_JSON(setNames(payload, NULL)) response$status <- 200L response$type <- 'json' + if (private$compress) + response$compress() TRUE }) @@ -397,6 +405,9 @@ Dash <- R6::R6Class( response$status <- 500L private$stack_message <- NULL } + + if (private$compress) + response$compress() TRUE }) @@ -444,10 +455,11 @@ Dash <- R6::R6Class( # if debug mode is not active dep_path <- system.file(dep_pkg$rpkg_path, package = dep_pkg$rpkg_name) - + response$body <- readLines(dep_path, warn = FALSE, encoding = "UTF-8") + if (!private$debug && has_fingerprint) { response$status <- 200L response$set_header('Cache-Control', @@ -472,6 +484,14 @@ Dash <- R6::R6Class( response$type <- get_mimetype(filename) } + if (private$compress && !is.null(response$body)) { + # reqres's compress method requires that body is a single + # string, but readLines returns a vector of strings for + # multi-line files + response$body <- paste(response$body, collapse="\n") + response$compress() + } + TRUE }) @@ -514,14 +534,20 @@ Dash <- R6::R6Class( response$body <- readLines(asset_path, warn = FALSE, encoding = "UTF-8") + + if (private$compress) { + response$body <- paste(response$body, collapse="\n") + response$compress() + } } else { file_handle <- file(asset_path, "rb") + response$body <- readBin(file_handle, raw(), file.size(asset_path)) close(file_handle) } - + response$status <- 200L } TRUE @@ -545,6 +571,9 @@ Dash <- R6::R6Class( ) response$type <- 'image/x-icon' response$status <- 200L + + if (private$compress) + response$compress() TRUE }) @@ -554,6 +583,9 @@ Dash <- R6::R6Class( response$body <- private$.index response$status <- 200L response$type <- 'html' + + if (private$compress) + response$compress() TRUE }) @@ -582,6 +614,7 @@ Dash <- R6::R6Class( response$body <- to_JSON(resp) response$status <- 200L response$type <- 'json' + # reset the field for the next reloading operation private$modified_since_reload <- list() TRUE @@ -880,6 +913,7 @@ Dash <- R6::R6Class( routes_pathname_prefix = NULL, requests_pathname_prefix = NULL, suppress_callback_exceptions = NULL, + compress = NULL, asset_map = NULL, css = NULL, scripts = NULL, From cfced69ed33dab8565839caf2cbeaea3c8011a9b Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 17 Dec 2019 02:54:29 -0500 Subject: [PATCH 16/28] sanity checks for response size --- R/dash.R | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/R/dash.R b/R/dash.R index dbe16735..437085b7 100644 --- a/R/dash.R +++ b/R/dash.R @@ -484,7 +484,7 @@ Dash <- R6::R6Class( response$type <- get_mimetype(filename) } - if (private$compress && !is.null(response$body)) { + if (private$compress && length(response$body) > 0) { # reqres's compress method requires that body is a single # string, but readLines returns a vector of strings for # multi-line files @@ -535,16 +535,17 @@ Dash <- R6::R6Class( warn = FALSE, encoding = "UTF-8") - if (private$compress) { + if (private$compress && length(response$body) > 0) { response$body <- paste(response$body, collapse="\n") response$compress() } } else { file_handle <- file(asset_path, "rb") + file_size <- file.size(asset_path) response$body <- readBin(file_handle, raw(), - file.size(asset_path)) + file_size) close(file_handle) } @@ -572,8 +573,6 @@ Dash <- R6::R6Class( response$type <- 'image/x-icon' response$status <- 200L - if (private$compress) - response$compress() TRUE }) From 72cb71713a9910b06799d852012ae3dc4f561676 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 17 Dec 2019 23:45:13 -0500 Subject: [PATCH 17/28] update remote refs --- DESCRIPTION | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index ef7267f5..85d62031 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -31,9 +31,9 @@ Collate: 'imports.R' 'print.R' 'internal.R' -Remotes: plotly/dash-html-components@17da1f4, - plotly/dash-core-components@cc1e654, - plotly/dash-table@042ad65 +Remotes: plotly/dash-html-components@64c2b1e, + plotly/dash-core-components@d171919, + plotly/dash-table@12caa6d License: MIT + file LICENSE Encoding: UTF-8 LazyData: true From df5888a09b206146dcc6603fa426f2e78fffed7c Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 18 Dec 2019 15:35:21 -0500 Subject: [PATCH 18/28] :bug: post-async fixes for CSS handling --- R/dash.R | 4 +++- R/utils.R | 20 ++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/R/dash.R b/R/dash.R index 437085b7..5961ca96 100644 --- a/R/dash.R +++ b/R/dash.R @@ -473,9 +473,11 @@ Dash <- R6::R6Class( request_etag <- request$headers[["If-None-Match"]] - if (modified == request_etag) { + if (!is.null(request_etag) && modified == request_etag) { response$body <- NULL response$status <- 304L + } else { + response$status <- 200L } } else { response$status <- 200L diff --git a/R/utils.R b/R/utils.R index 2cad0373..69eee8b7 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1169,8 +1169,10 @@ getDependencyPath <- function(dep) { if(!(is.null(dep$script))) { filename <- checkFingerprint(dep$script)[[1]] + dirname <- returnDirname(dep$script) } else { filename <- dep$stylesheet + dirname <- returnDirname(filename) } dep_path <- file.path(dep$src$file, filename) @@ -1184,7 +1186,7 @@ getDependencyPath <- function(dep) { # this may generate doubled slashes, which should not # pose problems on Mac OS, Windows, or Linux systems full_path_to_dependency <- system.file(file.path(dep$src$file, - returnDirname(dep$script), + dirname, filename), package=dep$package) @@ -1217,7 +1219,7 @@ getFileExt <- function(filepath) { returnDirname <- function(filepath) { dirname <- dirname(filepath) - if (dirname == ".") + if (is.null(dirname) || dirname == ".") return("") return(dirname) } @@ -1236,3 +1238,17 @@ isDynamic <- function(eager_loading, resource) { else return(TRUE) } + +compressResponse <- function(response, method = "deflate") { + if (method == "deflate") { + response$set_header("Content-Encoding", + "deflate") + response$body <- memCompress(response$body, "gzip") + } else if (method == "brotli") { + response$set_header("Content-Encoding", + "br") + #response$body <- paste(response$body, collapse="\n") + response$body <- brotli::brotli_compress(charToRaw(response$body)) + } + response +} From 0370ec8cb78f140ddcae5dbafaf57dfff1a301e5 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 18 Dec 2019 22:53:52 -0500 Subject: [PATCH 19/28] add tryCompress fn --- R/dash.R | 22 ++++++++-------------- R/utils.R | 28 +++++++++++++++++----------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/R/dash.R b/R/dash.R index 5961ca96..954bad42 100644 --- a/R/dash.R +++ b/R/dash.R @@ -247,8 +247,8 @@ Dash <- R6::R6Class( response$status <- 200L response$type <- 'json' - if (private$compress) - response$compress() + + TRUE }) @@ -276,7 +276,7 @@ Dash <- R6::R6Class( response$status <- 200L response$type <- 'json' if (private$compress) - response$compress() + response <- tryCompress(request, response) TRUE }) @@ -407,7 +407,7 @@ Dash <- R6::R6Class( } if (private$compress) - response$compress() + response <- tryCompress(request, response) TRUE }) @@ -486,13 +486,8 @@ Dash <- R6::R6Class( response$type <- get_mimetype(filename) } - if (private$compress && length(response$body) > 0) { - # reqres's compress method requires that body is a single - # string, but readLines returns a vector of strings for - # multi-line files - response$body <- paste(response$body, collapse="\n") - response$compress() - } + if (private$compress && length(response$body) > 0) + response <- tryCompress(request, response) TRUE }) @@ -538,8 +533,7 @@ Dash <- R6::R6Class( encoding = "UTF-8") if (private$compress && length(response$body) > 0) { - response$body <- paste(response$body, collapse="\n") - response$compress() + response <- tryCompress(request, response) } } else { file_handle <- file(asset_path, "rb") @@ -586,7 +580,7 @@ Dash <- R6::R6Class( response$type <- 'html' if (private$compress) - response$compress() + response <- tryCompress(request, response) TRUE }) diff --git a/R/utils.R b/R/utils.R index 69eee8b7..3e2a26e4 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1239,16 +1239,22 @@ isDynamic <- function(eager_loading, resource) { return(TRUE) } -compressResponse <- function(response, method = "deflate") { - if (method == "deflate") { - response$set_header("Content-Encoding", - "deflate") - response$body <- memCompress(response$body, "gzip") - } else if (method == "brotli") { - response$set_header("Content-Encoding", - "br") - #response$body <- paste(response$body, collapse="\n") - response$body <- brotli::brotli_compress(charToRaw(response$body)) +tryCompress <- function(request, response) { + # charToRaw requires a length one character string + response$body <- paste(response$body, collapse="\n") + # the reqres gzip implementation requires file I/O + # brotli does not; when available, use brotli with + # a moderate level of compression for speed -- + # the viewer pane only supports gzip and deflate, + # so gzip will be used when launching apps within + # RStudio + tryBrotli <- request$accepts_encoding('br') + if (tryBrotli == "br") { + response$body <- brotli::brotli_compress(charToRaw(response$body), + quality = 3) + response$set_header('Content-Encoding', + "br") + return(response) } - response + return(response$compress()) } From 2987a7a88715a4e3108f74218eb0574cc1b86b92 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 19 Dec 2019 00:10:57 -0500 Subject: [PATCH 20/28] :bug: dashLogger fails when self does not exist --- R/utils.R | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/R/utils.R b/R/utils.R index 3e2a26e4..90213e18 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1075,10 +1075,15 @@ dashLogger <- function(event = NULL, # is called from a private method within the Dash() R6 class; this makes # accessing variables set within Dash's private fields somewhat complicated # - # the following line retrieves the value of the silence_route_logging parameter, - # which is nearly 20 frames up the stack; if it's not found, we'll assume FALSE - silence_routes_logging <- dynGet("self", ifnotfound = FALSE)$config$silence_routes_logging - + # the following lines retrieve the value of the silence_route_logging parameter, + # which is many frames up the stack; if it's not found, we'll assume FALSE + self_object <- dynGet("self", ifnotfound = NULL) + + if (!is.null(self_object)) + silence_routes_logging <- self_object$config$silence_routes_logging + else + silence_routes_logging <- FALSE + if (!is.null(event)) { msg <- sprintf("%s: %s", event, message) From 63885ec1c93f9b0929cef22c7c95348e6cf27633 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 19 Dec 2019 02:07:56 -0500 Subject: [PATCH 21/28] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de472f62..a0ac3cb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file. ## Unreleased ### Added +- Support for asynchronous/dynamic loading of dependencies, resource caching, and asset fingerprinting [#157](https://github.com/plotly/dashR/pull/157) +- Compression of text resources using `brotli`, `gzip`, or `deflate` [#157](https://github.com/plotly/dashR/pull/157) - Support for adding `` tags to index [#142](https://github.com/plotly/dashR/pull/142) - Hot reloading now supported in debug mode [#127](https://github.com/plotly/dashR/pull/127) - Support for displaying Dash for R applications within RStudio's viewer pane when `use_viewer = TRUE` From 938b304cd56212c3616fc2162f740629a0f2e00e Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 19 Dec 2019 02:08:43 -0500 Subject: [PATCH 22/28] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0ac3cb2..78f2c67b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Change Log for Dash for R All notable changes to this project will be documented in this file. -## Unreleased +## [0.2.0] - 2019-12-19 ### Added - Support for asynchronous/dynamic loading of dependencies, resource caching, and asset fingerprinting [#157](https://github.com/plotly/dashR/pull/157) - Compression of text resources using `brotli`, `gzip`, or `deflate` [#157](https://github.com/plotly/dashR/pull/157) From a0c6bb6b742a4bea534b76ad17238c54fcebee8d Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 21 Dec 2019 19:11:23 -0500 Subject: [PATCH 23/28] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78f2c67b..7cd7f946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Change Log for Dash for R All notable changes to this project will be documented in this file. -## [0.2.0] - 2019-12-19 +## [0.2.0] - 2019-12-23 ### Added - Support for asynchronous/dynamic loading of dependencies, resource caching, and asset fingerprinting [#157](https://github.com/plotly/dashR/pull/157) - Compression of text resources using `brotli`, `gzip`, or `deflate` [#157](https://github.com/plotly/dashR/pull/157) From bd6a2d73b7659e2f17c3972637a8ebfd81887237 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 21 Dec 2019 19:11:32 -0500 Subject: [PATCH 24/28] fix commit hashes --- DESCRIPTION | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 85d62031..7a2d5289 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -31,9 +31,9 @@ Collate: 'imports.R' 'print.R' 'internal.R' -Remotes: plotly/dash-html-components@64c2b1e, - plotly/dash-core-components@d171919, - plotly/dash-table@12caa6d +Remotes: plotly/dash-html-components@55c3884, + plotly/dash-core-components@c107e0f, + plotly/dash-table@3058bd5 License: MIT + file LICENSE Encoding: UTF-8 LazyData: true From eb9521fea49f13e31caeefed49eb7b339fe76f5e Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 21 Dec 2019 19:23:01 -0500 Subject: [PATCH 25/28] update Dash help page --- man/Dash.Rd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/man/Dash.Rd b/man/Dash.Rd index 3fcd2858..b3e6dcfe 100644 --- a/man/Dash.Rd +++ b/man/Dash.Rd @@ -17,8 +17,10 @@ name = "dash", server = fiery::Fire$new(), assets_folder = 'assets', assets_url_path = '/assets', +eager_loading = FALSE, assets_ignore = '', serve_locally = TRUE, +meta_tags = NULL, routes_pathname_prefix = '/', requests_pathname_prefix = '/', external_scripts = NULL, @@ -39,11 +41,15 @@ for extra files to be used in the browser. Default is "assets". All .js and .css files will be loaded immediately unless excluded by \code{assets_ignore}, and other files such as images will be served if requested. Default is \code{assets}. \cr \code{assets_url_path} \tab \tab Character. Specify the URL path for asset serving. Default is \code{assets}. \cr +\code{eager_loading} \tab \tab Logical. Controls whether asynchronous resources are prefetched (if \code{TRUE}) or loaded on-demand (if \code{FALSE}). \cr \code{assets_ignore} \tab \tab Character. A regular expression, to match assets to omit from immediate loading. Ignored files will still be served if specifically requested. You cannot use this to prevent access to sensitive files. \cr \code{serve_locally} \tab \tab Whether to serve HTML dependencies locally or remotely (via URL).\cr +\code{meta_tags} \tab \tab List of lists. HTML \code{}tags to be added to the index page. +Each list element should have the attributes and values for one tag, eg: +\code{list(name = 'description', content = 'My App')}.\cr \code{routes_pathname_prefix} \tab \tab a prefix applied to the backend routes.\cr \code{requests_pathname_prefix} \tab \tab a prefix applied to request endpoints made by Dash's front-end.\cr From 4bea651eaf16955de5bfc0568dcf0a371fda9f46 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 21 Dec 2019 19:41:51 -0500 Subject: [PATCH 26/28] update URL to docs --- man/dash-package.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/dash-package.Rd b/man/dash-package.Rd index d4c2af3d..f17d0269 100644 --- a/man/dash-package.Rd +++ b/man/dash-package.Rd @@ -20,7 +20,7 @@ Dash is an open source package, released under the permissive MIT license. Plotl \seealso{ Useful links: \itemize{ - \item \url{http://dashr-docs.herokuapp.com} + \item \url{http://dashr.plot.ly} \item \url{https://github.com/plotly/dashR} \item Report bugs at \url{https://github.com/plotly/dashR/issues} } From 937715b0bc58f931b633f7fa9595a673b5403999 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 21 Dec 2019 20:12:27 -0500 Subject: [PATCH 27/28] :bug: fix example --- R/dash.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/R/dash.R b/R/dash.R index 954bad42..83b3392b 100644 --- a/R/dash.R +++ b/R/dash.R @@ -128,7 +128,10 @@ #' #' @examples #' \dontrun{ +#' library(dashCoreComponents) +#' library(dashHtmlComponents) #' library(dash) + #' app <- Dash$new() #' app$layout( #' dccInput(id = "inputID", value = "initial value", type = "text"), From a385ede62ba45b2e5dacab5ffeabc9c7cec740e8 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 21 Dec 2019 20:39:15 -0500 Subject: [PATCH 28/28] fix example; load pkgs --- man/Dash.Rd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/man/Dash.Rd b/man/Dash.Rd index b3e6dcfe..a27f4a62 100644 --- a/man/Dash.Rd +++ b/man/Dash.Rd @@ -142,6 +142,8 @@ to the \code{ignite()} method of the \link[fiery:Fire]{fiery::Fire} server. \examples{ \dontrun{ +library(dashCoreComponents) +library(dashHtmlComponents) library(dash) app <- Dash$new() app$layout(