diff --git a/DESCRIPTION b/DESCRIPTION index 6b4e427e..b84e052b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -32,6 +32,7 @@ Imports: glue, lintr (>= 3.0.0), logger, + processx, purrr, renv, rstudioapi, diff --git a/NAMESPACE b/NAMESPACE index 3378e8db..914f4372 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ export(app) export(build_js) export(build_sass) +export(dev) export(diagnostics) export(format_js) export(format_r) diff --git a/R/node.R b/R/node.R index 7f0fbae9..f5569489 100644 --- a/R/node.R +++ b/R/node.R @@ -2,36 +2,85 @@ node_path <- function(...) { fs::path(".rhino", ...) } +npm_command <- function() { + Sys.getenv("RHINO_NPM", "npm") +} + +# Run a script defined in `package.json`. +npm_run <- function(...) { + # Use `--silent` to prevent `npm` from echoing the command it runs. + # The output of the script itself will still be displayed. + npm("--silent", "run", ...) +} + # Run `npm` or an alternative command specified by `RHINO_NPM`. # If needed, copy over Node.js template and install dependencies. npm <- function(...) { - npm_command <- Sys.getenv("RHINO_NPM", "npm") - check_system_dependency( - cmd = npm_command, - dependency_name = ifelse(npm_command == "npm", "Node.js", npm_command), - documentation_url = "https://go.appsilon.com/rhino-system-dependencies" + node <- node_check() + if (!node$status_ok) { + node_missing(abort = TRUE) + } + node_init() + node_run(..., wd = node_path()) +} + +node_check <- function() { + version <- tryCatch( + node_run("--version", stdout = "|")$stdout, + error = function(e) NULL + ) + list( + status_ok = !is.null(version), + diagnostic_info = paste(npm_command(), ifelse(is.null(version), "failed", trimws(version))) ) - node_init(npm_command) - node_run(npm_command, ...) } -node_init <- function(npm_command) { +node_missing <- function(info = NULL, abort = FALSE) { + docs_url <- "https://go.appsilon.com/rhino-system-dependencies" # nolint object_usage_linter + msg <- c( + "!" = "Failed to run system command {.code {npm_command()}}.", + " " = "Do you have Node.js installed? Read more: {.url {docs_url}}", + "i" = info + ) + if (abort) { + cli::cli_abort(msg) + } else { + cli::cli_bullets(msg) + } +} + +node_init <- function() { if (!fs::dir_exists(node_path())) { cli::cli_alert_info("Initializing Node.js directory...") copy_template("node", node_path()) } if (!fs::dir_exists(node_path("node_modules"))) { - cli::cli_alert_info("Installing Node.js packages with {npm_command}...") - node_run(npm_command, "install", "--no-audit", "--no-fund") + cli::cli_alert_info("Installing Node.js packages with {.code {npm_command()}}...") + node_run("install", "--no-audit", "--no-fund", wd = node_path()) } } # Run the specified command in Node.js directory (assume it already exists). -node_run <- function(command, ..., status_ok = 0) { - withr::with_dir(node_path(), { - status <- system2(command = command, args = c(...)) - }) - if (status != status_ok) { - cli::cli_abort("System command '{command}' exited with status {status}.") +node_run <- function(..., stdout = NULL, wd = NULL, background = FALSE) { + if (background) { + run <- processx::process$new + } else { + run <- processx::run } + + # Workaround: {processx} cannot find `npm` on Windows, but it works with a shell. + if (.Platform$OS.type == "windows") { + command <- "cmd" + args <- rlang::chr("/c", npm_command(), ...) + } else { + command <- npm_command() + args <- rlang::chr(...) + } + + run( + command = command, + args = args, + stdout = stdout, + wd = wd + ) } diff --git a/R/rhino.R b/R/rhino.R index f605a179..4d638bc7 100644 --- a/R/rhino.R +++ b/R/rhino.R @@ -48,35 +48,6 @@ copy_rproj <- function() { ) } -system_cmd_version <- function(cmd, throw_error = FALSE) { - tryCatch( - system2(cmd, "--version", stdout = TRUE, stderr = TRUE), - error = function(e) { - if (isTRUE(throw_error)) cli::cli_abort(e) - - e$message - } - ) -} - -check_system_dependency <- function( - cmd, - dependency_name, - documentation_url, - additional_message = NULL -) { - message <- c( - glue::glue("Do you have {dependency_name} installed?"), - glue::glue("Check {documentation_url} for details."), - additional_message - ) - - tryCatch( - system_cmd_version(cmd, TRUE), - error = function(e) cli::cli_abort(message) - ) -} - #' Print diagnostics #' #' Prints information which can be useful for diagnosing issues with Rhino. @@ -93,7 +64,7 @@ diagnostics <- function() { writeLines(c( paste(Sys.info()[c("sysname", "release", "version")], collapse = " "), R.version.string, - paste("rhino:", utils::packageVersion("rhino")), - paste("node:", system_cmd_version("node")) + paste("rhino", utils::packageVersion("rhino")), + node_check()$diagnostic_info )) } diff --git a/R/tools.R b/R/tools.R index 0330f5a0..63dd8113 100644 --- a/R/tools.R +++ b/R/tools.R @@ -1,3 +1,58 @@ +#' Development mode +#' +#' Run application in development mode with automatic rebuilding and reloading. +#' +#' This function will launch the Shiny app in +#' [development mode](https://shiny.posit.co/r/reference/shiny/latest/devmode.html) +#' (as if `options(shiny.devmode = TRUE)` was set). +#' The app will be automatically reloaded whenever the sources change. +#' +#' Additionally, Rhino will automatically rebuild JavaScript and Sass in the background. +#' Please note that this feature requires Node.js. +#' +#' @param build_js Boolean. Rebuild JavaScript automatically in the background? +#' @param build_sass Boolean. Rebuild Sass automatically in the background? +#' @param ... Additional arguments passed to `shiny::runApp()`. +#' @return None. This function is called for side effects. +#' +#' @export +dev <- function(build_js = TRUE, build_sass = TRUE, ...) { + proc <- dev_build(build_js, build_sass) + if (!is.null(proc)) on.exit(proc$kill()) + shiny::with_devmode(TRUE, shiny::runApp(...)) +} + +dev_build <- function(build_js, build_sass) { + if (!build_js && !build_sass) return() + + node <- node_check() + if (!node$status_ok) { + node_missing(node$npm_command, info = "JavaScript and Sass won't be automatically rebuilt.") + return() + } + + if (build_sass) { + config <- read_config() + if (config$sass != "node") { + build_sass <- FALSE + cli::cli_bullets(c( + "!" = "Sass won't be automatically rebuilt.", + "i" = "Use {.code sass: node} configuration in {.file rhino.yml} to enable it." + )) + } + } + + # Is there is anything to do? Check again - building Sass might have been disabled. + if (!build_js && !build_sass) return() + + npm_run( + "concurrently", "--", + if (build_js) "npm:build-js -- --watch", + if (build_sass) "npm:build-sass -- --watch", + background = TRUE + ) +} + #' Run R unit tests #' #' Uses the `{testhat}` package to run all unit tests in `tests/testthat` directory. @@ -234,9 +289,9 @@ format_r <- function(paths, exclude_files = NULL) { #' @export build_js <- function(watch = FALSE) { if (watch) { - npm("run", "build-js", "--", "--watch", status_ok = 2) + npm_run("build-js", "--", "--watch") } else { - npm("run", "build-js") + npm_run("build-js") } } @@ -273,9 +328,9 @@ build_js <- function(watch = FALSE) { # nolint end lint_js <- function(fix = FALSE) { if (fix) { - npm("run", "lint-js", "--", "--fix") + npm_run("lint-js", "--", "--fix") } else { - npm("run", "lint-js") + npm_run("lint-js") } } @@ -296,9 +351,9 @@ lint_js <- function(fix = FALSE) { #' @export format_js <- function(fix = TRUE) { if (fix) { - npm("run", "format-js", "--", "--write") + npm_run("format-js", "--", "--write") } else { - npm("run", "format-js", "--", "--check") + npm_run("format-js", "--", "--check") } } @@ -356,9 +411,9 @@ build_sass <- function(watch = FALSE) { build_sass_node <- function(watch = FALSE) { if (watch) { - npm("run", "build-sass", "--", "--watch", status_ok = 2) + npm_run("build-sass", "--", "--watch") } else { - npm("run", "build-sass") + npm_run("build-sass") } } @@ -389,9 +444,9 @@ build_sass_r <- function() { #' @export lint_sass <- function(fix = FALSE) { if (fix) { - npm("run", "lint-sass", "--", "--fix") + npm_run("lint-sass", "--", "--fix") } else { - npm("run", "lint-sass") + npm_run("lint-sass") } } @@ -412,9 +467,9 @@ lint_sass <- function(fix = FALSE) { #' @export format_sass <- function(fix = TRUE) { if (fix) { - npm("run", "format-sass", "--", "--write") + npm_run("format-sass", "--", "--write") } else { - npm("run", "format-sass", "--", "--check") + npm_run("format-sass", "--", "--check") } } @@ -445,8 +500,8 @@ format_sass <- function(fix = TRUE) { #' @export test_e2e <- function(interactive = FALSE) { if (interactive) { - npm("run", "test-e2e-interactive") + npm_run("test-e2e-interactive") } else { - npm("run", "test-e2e") + npm_run("test-e2e") } } diff --git a/inst/templates/node/package-lock.json b/inst/templates/node/package-lock.json index 76b9d655..57ef5b7b 100644 --- a/inst/templates/node/package-lock.json +++ b/inst/templates/node/package-lock.json @@ -11,6 +11,7 @@ "@babel/preset-env": "^7.23.7", "@babel/preset-react": "^7.23.3", "babel-loader": "^9.1.3", + "concurrently": "^8.2.2", "cypress": "^13.6.2", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", @@ -3555,6 +3556,21 @@ "node": ">=8" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "dev": true, @@ -3639,6 +3655,34 @@ "dev": true, "license": "MIT" }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -3842,6 +3886,23 @@ "node": ">=0.10" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/dayjs": { "version": "1.10.7", "dev": true, @@ -5283,6 +5344,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -7578,6 +7649,16 @@ "throttleit": "^1.0.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "dev": true, @@ -7868,6 +7949,16 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.0.4", "dev": true, @@ -7937,6 +8028,12 @@ "source-map": "^0.6.0" } }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/spdx-correct": { "version": "3.1.1", "dev": true, @@ -8566,6 +8663,16 @@ "node": ">= 4.0.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim-newlines": { "version": "3.0.1", "dev": true, @@ -9142,6 +9249,16 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "dev": true, @@ -9156,6 +9273,25 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yargs-parser": { "version": "20.2.9", "dev": true, @@ -9164,6 +9300,16 @@ "node": ">=10" } }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yauzl": { "version": "2.10.0", "dev": true, @@ -11691,6 +11837,17 @@ } } }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, "clone-deep": { "version": "4.0.1", "dev": true, @@ -11751,6 +11908,23 @@ "version": "0.0.1", "dev": true }, + "concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + } + }, "confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -11909,6 +12083,15 @@ "assert-plus": "^1.0.0" } }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "dayjs": { "version": "1.10.7", "dev": true @@ -12945,6 +13128,12 @@ "version": "1.0.0-beta.2", "dev": true }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, "get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -14454,6 +14643,12 @@ "throttleit": "^1.0.0" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, "require-from-string": { "version": "2.0.2", "dev": true @@ -14636,6 +14831,12 @@ "version": "3.0.0", "dev": true }, + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true + }, "side-channel": { "version": "1.0.4", "dev": true, @@ -14682,6 +14883,12 @@ "source-map": "^0.6.0" } }, + "spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "spdx-correct": { "version": "3.1.1", "dev": true, @@ -15131,6 +15338,12 @@ } } }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "trim-newlines": { "version": "3.0.1", "dev": true @@ -15525,6 +15738,12 @@ "signal-exit": "^3.0.7" } }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, "yallist": { "version": "4.0.0", "dev": true @@ -15535,6 +15754,29 @@ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "dependencies": { + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "yargs-parser": { "version": "20.2.9", "dev": true diff --git a/inst/templates/node/package.json b/inst/templates/node/package.json index 8d62ba4d..66bb9e13 100644 --- a/inst/templates/node/package.json +++ b/inst/templates/node/package.json @@ -1,8 +1,9 @@ { "private": true, "scripts": { + "concurrently": "concurrently", "build-js": "webpack", - "build-sass": "sass --no-source-map --style=compressed ../app/styles/main.scss:../app/static/css/app.min.css", + "build-sass": "sass --no-source-map --style=compressed --color ../app/styles/main.scss:../app/static/css/app.min.css", "lint-js": "eslint --config .eslintrc.json ../app/js", "lint-sass": "stylelint ../app/styles", "format-js": "prettier --config prettier.config.mjs --ignore-path none ../app/js/**/*.js", @@ -19,6 +20,7 @@ "@babel/preset-env": "^7.23.7", "@babel/preset-react": "^7.23.3", "babel-loader": "^9.1.3", + "concurrently": "^8.2.2", "cypress": "^13.6.2", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", diff --git a/man/dev.Rd b/man/dev.Rd new file mode 100644 index 00000000..dafb8894 --- /dev/null +++ b/man/dev.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tools.R +\name{dev} +\alias{dev} +\title{Development mode} +\usage{ +dev(build_js = TRUE, build_sass = TRUE, ...) +} +\arguments{ +\item{build_js}{Boolean. Rebuild JavaScript automatically in the background?} + +\item{build_sass}{Boolean. Rebuild Sass automatically in the background?} + +\item{...}{Additional arguments passed to \code{shiny::runApp()}.} +} +\value{ +None. This function is called for side effects. +} +\description{ +Run application in development mode with automatic rebuilding and reloading. +} +\details{ +This function will launch the Shiny app in +\href{https://shiny.posit.co/r/reference/shiny/latest/devmode.html}{development mode} +(as if \code{options(shiny.devmode = TRUE)} was set). +The app will be automatically reloaded whenever the sources change. + +Additionally, Rhino will automatically rebuild JavaScript and Sass in the background. +Please note that this feature requires Node.js. +} diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index e1cc4812..48a8c78c 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -141,6 +141,7 @@ reference: - title: R development contents: + - dev - dependencies - log - format_r