diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 9ff2ecdf..2be0b0f1 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -65,6 +65,13 @@ jobs: cd RhinoApp Rscript ../test-dependencies.R + - name: Node.js commands should respect RHINO_NPM + # Skip this test on Windows because it requires a Unix shell. + if: runner.os != 'Windows' + run: | + cd RhinoApp + Rscript ../test-custom-npm.R + - name: lint_r() should detect lint errors in R scripts if: always() run: | diff --git a/DESCRIPTION b/DESCRIPTION index 6708609a..07015650 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: rhino Title: A Framework for Enterprise Shiny Applications -Version: 1.5.0.9003 +Version: 1.5.0.9004 Authors@R: c( person("Kamil", "Żyła", role = c("aut", "cre"), email = "opensource+kamil@appsilon.com"), diff --git a/NEWS.md b/NEWS.md index 4d26de0d..1076f588 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,8 @@ * `lint_sass()` now uses `stylelint` 14.16 (the last major version supporting stylistic rules) * Upgrade all remaining Node.js dependencies to latest versions and fix vulnerabilities. * The minimum supported Node.js version is now 16. +4. Introduce `RHINO_NPM` environment variable +to allow using `npm` alternatives like `bun` and `pnpm`. # [rhino 1.5.0](https://github.com/Appsilon/rhino/releases/tag/v1.5.0) diff --git a/R/node.R b/R/node.R index 2263eb64..7f0fbae9 100644 --- a/R/node.R +++ b/R/node.R @@ -2,33 +2,36 @@ node_path <- function(...) { fs::path(".rhino", ...) } -add_node <- function(clean = FALSE) { - if (clean && fs::dir_exists(node_path())) { - fs::dir_delete(node_path()) +# 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_init(npm_command) + node_run(npm_command, ...) +} + +node_init <- function(npm_command) { + 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") } - copy_template("node", node_path()) } -# Run `npm` command (assume node directory already exists in the project). -npm_raw <- function(..., status_ok = 0) { +# 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 = "npm", args = c(...)) + status <- system2(command = command, args = c(...)) }) if (status != status_ok) { - cli::cli_abort("System command 'npm' exited with status {status}.") - } -} - -# Run `npm` command (create node directory in the project if needed). -npm <- function(...) { - check_system_dependency( - cmd = "node", - dependency_name = "Node.js", - documentation_url = "https://go.appsilon.com/rhino-system-dependencies" - ) - if (!fs::dir_exists(node_path())) { - add_node() - npm_raw("install", "--no-audit", "--no-fund") + cli::cli_abort("System command '{command}' exited with status {status}.") } - npm_raw(...) } diff --git a/inst/WORDLIST b/inst/WORDLIST index 70df777b..c89b7b65 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -1,3 +1,5 @@ +Addin +Addins Appsilon Boxifying ESLint @@ -13,9 +15,11 @@ Renv Renviron Rhinoverse Rprofile +Rstudio SDK Stylelint UI +Webpack blogpost conf config @@ -41,6 +45,7 @@ nodejs npm nvm overridable +pnpm preconfigured renv roxygen @@ -54,5 +59,3 @@ unintuitive usethis webpack yml -Addin -Addins diff --git a/tests/e2e/test-custom-npm.R b/tests/e2e/test-custom-npm.R new file mode 100644 index 00000000..91f43a44 --- /dev/null +++ b/tests/e2e/test-custom-npm.R @@ -0,0 +1,22 @@ +local({ + tmp <- withr::local_tempdir() + wrapper_path <- fs::path(tmp, "wrapper") + touch_path <- fs::path(tmp, "it_works") + + # Prepare a wrapper script which creates an "it_works" file and runs npm. + fs::file_create(wrapper_path, mode = "u=rwx") + writeLines( + c( + "#!/bin/sh", + paste("touch", touch_path), + 'exec npm "$@"' + ), + wrapper_path + ) + + # Use the wrapper script instead of npm. + withr::local_envvar(RHINO_NPM = wrapper_path) + rhino:::npm("--version") + + testthat::expect_true(fs::file_exists(touch_path)) +}) diff --git a/vignettes/explanation/node-js-javascript-and-sass-tools.Rmd b/vignettes/explanation/node-js-javascript-and-sass-tools.Rmd index e9044fd9..b896e43d 100644 --- a/vignettes/explanation/node-js-javascript-and-sass-tools.Rmd +++ b/vignettes/explanation/node-js-javascript-and-sass-tools.Rmd @@ -13,7 +13,13 @@ vignette: > can execute JavaScript code outside a web browser. It is used widely for web development. Its package manager, [npm](https://docs.npmjs.com/about-npm), makes it easy to install -virtually any JavaScript library. +virtually any JavaScript library. You can use other package managers such as +[bun](https://bun.sh) and [pnpm](https://pnpm.io/) that are compatible with +`npm`. + +To switch from the default npm usage, set a global environment variable named +`RHINO_NPM`. For instance, if you want to use `bun` instead of `npm`, +add `export RHINO_NPM=bun` to your shell startup file (e.g. `.bashrc`). Rhino uses Node.js to provide state of the art tools for working with JavaScript and Sass. The following functions require Node.js to work: @@ -26,7 +32,7 @@ JavaScript and Sass. The following functions require Node.js to work: ### Node directory -Under the hood Rhino will create a `.rhino/node` directory in your +Under the hood Rhino will create a `.rhino` directory in your project to store the specific libraries needed by these tools. This directory is git-ignored by default and safe to remove.