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/tools.R b/R/tools.R index b7550b87..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. 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