├── .Rbuildignore ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml └── workflows │ ├── app-push-test.yml │ ├── ci.yml │ ├── e2e-test.yml │ └── pkgdown.yml ├── .gitignore ├── .lintr ├── DESCRIPTION ├── NAMESPACE ├── NEWS.md ├── R ├── addins.R ├── app.R ├── config.R ├── data.R ├── dependencies.R ├── destructure.R ├── init.R ├── linters.R ├── log.R ├── node.R ├── react.R ├── rhino.R └── tools.R ├── README.md ├── cran-comments.md ├── data └── rhinos.rda ├── inst ├── WORDLIST ├── rstudio │ ├── addins.dcf │ ├── addins │ │ ├── build_js.R │ │ ├── build_js_watch.R │ │ ├── build_sass.R │ │ ├── build_sass_watch.R │ │ ├── lint_js.R │ │ ├── lint_js_fix.R │ │ ├── lint_r.R │ │ ├── lint_sass.R │ │ ├── lint_sass_fix.R │ │ ├── module.R │ │ ├── test_e2e.R │ │ ├── test_e2e_int.R │ │ └── test_r.R │ └── templates │ │ └── project │ │ ├── favicon.ico │ │ └── init.dcf └── templates │ ├── app_structure │ ├── app.R │ ├── app │ │ ├── js │ │ │ └── index.js │ │ ├── logic │ │ │ └── __init__.R │ │ ├── main.R │ │ ├── static │ │ │ └── favicon.ico │ │ ├── styles │ │ │ └── main.scss │ │ └── view │ │ │ └── __init__.R │ ├── config.yml │ ├── dot.lintr │ ├── dot.rscignore │ └── rhino.yml │ ├── e2e_tests │ └── tests │ │ ├── cypress.config.js │ │ └── cypress │ │ ├── dot.gitignore │ │ └── e2e │ │ └── app.cy.js │ ├── github_ci │ └── dot.github │ │ └── workflows │ │ └── rhino-test.yml │ ├── node │ ├── babel.config.json │ ├── dot.eslintrc.json │ ├── dot.gitignore │ ├── dot.stylelintrc.json │ ├── package-lock.json │ ├── package.json │ ├── prettier.config.mjs │ └── webpack.config.js │ ├── renv │ ├── dot.Rprofile │ └── dot.renvignore │ ├── rproj │ └── Rproj.template │ └── unit_tests │ └── tests │ └── testthat │ └── test-main.R ├── man ├── app.Rd ├── auto_test_r.Rd ├── build_js.Rd ├── build_sass.Rd ├── dependencies.Rd ├── devmode.Rd ├── diagnostics.Rd ├── figures │ ├── lifecycle-deprecated.svg │ ├── lifecycle-experimental.svg │ ├── lifecycle-stable.svg │ ├── lifecycle-superseded.svg │ └── rhino.png ├── format_js.Rd ├── format_r.Rd ├── format_sass.Rd ├── grapes-set-grapes.Rd ├── init.Rd ├── lint_js.Rd ├── lint_r.Rd ├── lint_sass.Rd ├── log.Rd ├── react_component.Rd ├── rhinos.Rd ├── test_e2e.Rd └── test_r.Rd ├── pkgdown ├── _pkgdown.yml ├── build.R ├── extra.css ├── favicon │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico └── versions.yml ├── rhino.Rproj ├── tests ├── e2e │ ├── app-files │ │ ├── Box.jsx │ │ ├── config.yml │ │ ├── hello.R │ │ ├── hello.cy.js │ │ ├── index.js │ │ ├── main.R │ │ ├── main.scss │ │ ├── say_hello.R │ │ ├── test-hello.R │ │ └── test-say_hello.R │ ├── test-box-lsp.R │ ├── test-build-js.R │ ├── test-build-sass.R │ ├── test-custom-npm.R │ ├── test-dependencies.R │ ├── test-format-js.R │ ├── test-format-r.R │ ├── test-format-sass.R │ ├── test-lint-js.R │ ├── test-lint-r.R │ └── test-lint-sass.R ├── testthat.R └── testthat │ ├── helpers │ └── main.scss │ ├── test-app.R │ ├── test-config.R │ ├── test-dependencies.R │ ├── test-destructure.R │ ├── test-rhino.R │ └── test-tools.R └── vignettes ├── explanation ├── application-structure.Rmd ├── box-modules.Rmd ├── node-js-javascript-and-sass-tools.Rmd ├── renv-configuration.Rmd ├── rhino-style-guide.Rmd ├── rhino-yml.Rmd └── what-is-rhino.Rmd ├── faq.Rmd ├── how-to ├── add-internationalization.Rmd ├── add-routing.Rmd ├── box-lsp.Rmd ├── build-rhino-apps-with-llm-tools.Rmd ├── communicate-between-modules.Rmd ├── enable-shiny-bookmarking.Rmd ├── images │ ├── communicate_between_modules_1.png │ ├── communicate_between_modules_2.png │ └── rhino_addins.png ├── keep-multiple-apps-in-a-single-repository.Rmd ├── manage-secrets-and-environments.Rmd ├── migrate-1-10.Rmd ├── migrate-1-6.Rmd ├── migrate-1-7.Rmd ├── migrate-1-8.Rmd ├── migrate-1-9.Rmd ├── migrate-app-to-rhino.Rmd ├── publish-on-huggingface.Rmd ├── set-application-run-parameters.Rmd ├── use-addins.Rmd ├── use-bslib.Rmd ├── use-destructure-operator.Rmd ├── use-global-variables.Rmd ├── use-polished.Rmd ├── use-shiny-fluent.Rmd ├── use-shinymanager.Rmd ├── use-shinytest2.Rmd ├── use-static-files.Rmd ├── write-javascript-code.Rmd └── write-r-code.Rmd └── tutorial ├── create-your-first-rhino-app.Rmd ├── images ├── basic_e2e_test.png ├── chart.png ├── chart_2.png ├── clicks_e2e_test_1.png ├── clicks_e2e_test_2.png ├── cypress_test_app.gif ├── e2e_in_CI.png ├── failing_e2e_test.png ├── first_module.png ├── init_application.png ├── interactive_e2e_test.png ├── js_1.png ├── js_2.png ├── message_e2e_test_1.png ├── message_e2e_test_2.png ├── rstudio_wizard.png ├── styles_1.png ├── styles_2.png ├── table_1.png ├── table_2.png └── table_3.png ├── use-react-in-rhino.Rmd └── write-end-to-end-tests-with-cypress.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.git$ 3 | ^\.github$ 4 | ^\.gitignore$ 5 | ^\.lintr$ 6 | ^\.Rproj\.user$ 7 | ^pkgdown$ 8 | ^README\.md$ 9 | ^vignettes$ 10 | ^docs$ 11 | ^cran-comments\.md$ 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a problem you encountered 3 | labels: ['status: triage'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: > 8 | Thank you for taking the time to complete this bug report! 9 | Please [search](https://github.com/Appsilon/rhino/issues) 10 | through existing issues first to make sure you are not creating a duplicate. 11 | 12 | - type: textarea 13 | attributes: 14 | label: Steps to reproduce 15 | value: '1. ' 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | attributes: 21 | label: Bug description 22 | description: What happened? Attach error messages and screenshots if applicable. 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | attributes: 28 | label: Expected behavior 29 | description: What should have happened? 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | attributes: 35 | label: Rhino diagnostics 36 | description: Paste the output of `rhino::diagnostics()` below. 37 | 38 | - type: textarea 39 | attributes: 40 | label: Comments 41 | description: Add any other useful information here. 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Documentation 4 | url: https://appsilon.github.io/rhino/ 5 | about: Learn about Rhino from our extensive documentation 6 | - name: Discussions 7 | url: https://github.com/Appsilon/rhino/discussions 8 | about: Use the board for questions and general discussion 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Request a new feature 3 | labels: ['status: triage'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: > 8 | Thank you for taking the time to complete this feature request! 9 | Please [search](https://github.com/Appsilon/rhino/issues) 10 | through existing issues first to make sure you are not creating a duplicate. 11 | 12 | - type: textarea 13 | attributes: 14 | label: Motivation 15 | description: What is your need? What problem are you trying to solve? 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | attributes: 21 | label: Feature description 22 | description: How would this feature work from user's perspective? 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | attributes: 28 | label: Implementation ideas 29 | description: How would this feature work under the hood? 30 | 31 | - type: textarea 32 | attributes: 33 | label: Impact 34 | description: How often would this feature be used? What if we don't implement it? 35 | 36 | - type: textarea 37 | attributes: 38 | label: Comments 39 | description: Add any other useful information here. 40 | -------------------------------------------------------------------------------- /.github/workflows/app-push-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow initializes a fresh Rhino app and pushes it to a dedicated orphan branch. 2 | # This way we can test the app CI (the GitHub Actions workflow created on `rhino::init()`). 3 | name: App Push Test 4 | on: 5 | push: 6 | branches: [main] 7 | workflow_dispatch: 8 | permissions: {} 9 | jobs: 10 | main: 11 | name: Push fresh app 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 30 14 | env: 15 | BRANCH_NAME: bot/app-push-test 16 | steps: 17 | - name: Install R 18 | uses: r-lib/actions/setup-r@v2 19 | with: 20 | use-public-rspm: true 21 | 22 | - name: Install Rhino 23 | uses: r-lib/actions/setup-r-dependencies@v2 24 | with: 25 | packages: Appsilon/rhino@${{ github.sha }} 26 | 27 | # The previous step installs `renv` using `pak`, 28 | # but it subsequentially fails in the pushed app and breaks the test 29 | # (see https://github.com/rstudio/renv/issues/1772). 30 | # This step is is a workaround and can be removed once the issue is resolved. 31 | - name: Install renv 32 | shell: Rscript {0} 33 | run: install.packages("renv") 34 | 35 | - name: Checkout repository 36 | uses: actions/checkout@v3 37 | with: 38 | # The default GITHUB_PAT has insufficient permissions to push workflow changes. 39 | # The token passed here must have write access to code and workflows. 40 | token: ${{ secrets.APP_PUSH_TEST_PAT }} 41 | 42 | - name: Prepare branch 43 | run: git switch --orphan "$BRANCH_NAME" 44 | 45 | - name: Initialize app 46 | shell: Rscript {0} 47 | run: rhino::init() 48 | 49 | - name: Push branch 50 | run: | 51 | SHORT_SHA=$(printf %.8s "$GITHUB_SHA") 52 | git config user.name "$GITHUB_ACTOR" 53 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 54 | git add . 55 | git commit --message "App Push Test (${GITHUB_REF_NAME}@${SHORT_SHA})" 56 | git push --force origin "$BRANCH_NAME" 57 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | workflow_dispatch: 8 | permissions: 9 | contents: read 10 | jobs: 11 | main: 12 | name: Check, lint & test - ${{ matrix.config.os }} (${{ matrix.config.r }}) 13 | runs-on: ${{ matrix.config.os }} 14 | timeout-minutes: 30 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | config: 19 | - {os: macOS-latest, r: 'release'} 20 | - {os: windows-latest, r: 'release'} 21 | - {os: ubuntu-22.04, r: 'devel'} 22 | - {os: ubuntu-22.04, r: 'release'} 23 | - {os: ubuntu-22.04, r: 'oldrel'} 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v3 28 | 29 | - name: Install R 30 | uses: r-lib/actions/setup-r@v2 31 | with: 32 | use-public-rspm: true 33 | 34 | - name: Install R package dependencies 35 | uses: r-lib/actions/setup-r-dependencies@v2 36 | with: 37 | extra-packages: local::. # Necessary to avoid object usage linter errors. 38 | 39 | - name: R CMD check 40 | if: always() 41 | uses: r-lib/actions/check-r-package@v2 42 | with: 43 | error-on: '"note"' 44 | 45 | - name: Lint 46 | if: always() 47 | shell: Rscript {0} 48 | run: | 49 | lints <- lintr::lint_package() 50 | for (lint in lints) print(lint) 51 | quit(status = length(lints) > 0) 52 | 53 | - name: Test 54 | if: always() 55 | shell: Rscript {0} 56 | run: | 57 | # Errors will fail this step automatically. 58 | tests <- testthat::test_local() 59 | 60 | # Escalate warnings to a failure too. 61 | expectations <- 62 | lapply(tests, function(test) { 63 | lapply(test$results, class) 64 | }) |> 65 | unlist() 66 | warnings_found <- any(grepl("expectation_warning", expectations)) 67 | quit(status = warnings_found) 68 | 69 | - name: Test coverage 70 | if: always() 71 | env: 72 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 73 | shell: Rscript {0} 74 | run: covr::codecov() 75 | 76 | - name: Spell check 77 | if: always() 78 | shell: Rscript {0} 79 | run: | 80 | spell_check <- spelling::spell_check_package(use_wordlist = TRUE) 81 | cli::cli_alert_warning("There are {nrow(spell_check)} spelling error{?s}.") 82 | if (nrow(spell_check) > 0) { 83 | print(spell_check) 84 | } 85 | quit(status = nrow(spell_check) > 0) 86 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yml: -------------------------------------------------------------------------------- 1 | name: pkgdown 2 | on: 3 | push: 4 | branches: [main] 5 | permissions: 6 | contents: write 7 | jobs: 8 | pkgdown: 9 | name: Build and publish website 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 30 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install R 19 | uses: r-lib/actions/setup-r@v2 20 | with: 21 | use-public-rspm: true # Dramatically speeds up installation of dependencies. 22 | 23 | - name: Install R package dependencies 24 | uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::pkgdown, local::. 27 | 28 | - name: Build site 29 | shell: Rscript {0} 30 | run: | 31 | source("pkgdown/build.R") 32 | build_versioned( 33 | repo = ".", 34 | versions = yaml::read_yaml("pkgdown/versions.yml"), 35 | root_url = "https://appsilon.github.io/rhino", 36 | destination = "docs" 37 | ) 38 | 39 | - name: Deploy 40 | uses: JamesIves/github-pages-deploy-action@v4 41 | with: 42 | folder: docs 43 | branch: bot/github-pages 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | /docs/ 3 | -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | linters: 2 | linters_with_defaults( 3 | line_length_linter = line_length_linter(100) 4 | ) 5 | exclusions: 6 | c( 7 | "inst/rstudio", 8 | "inst/templates", 9 | "tests/e2e/app-files" 10 | ) 11 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: rhino 2 | Title: A Framework for Enterprise Shiny Applications 3 | Version: 1.11.0.9000 4 | Authors@R: 5 | c( 6 | person("Kamil", "Żyła", role = c("aut", "cre"), email = "opensource+kamil@appsilon.com"), 7 | person("Jakub", "Nowicki", role = "aut", email = "kuba@appsilon.com"), 8 | person("Leszek", "Siemiński", role = "aut", email = "leszek.sieminski@appsilon.com"), 9 | person("Marek", "Rogala", role = "aut", email = "marek@appsilon.com"), 10 | person("Recle", "Vibal", role = "aut", email = "recle.vibal@appsilon.com"), 11 | person("Tymoteusz", "Makowski", role = "aut", email = "tymoteusz@appsilon.com"), 12 | person("Rodrigo", "Basa", role = "aut", email = "rodrigo@appsilon.com"), 13 | person("Eduardo", "Almeida", role = "ctb", email = "eduardo@appsilon.com"), 14 | person("Appsilon Sp. z o.o.", role = "cph", email = "opensource@appsilon.com") 15 | ) 16 | Description: A framework that supports creating and extending enterprise Shiny applications using best practices. 17 | URL: https://appsilon.github.io/rhino/, https://github.com/Appsilon/rhino 18 | BugReports: https://github.com/Appsilon/rhino/issues 19 | License: LGPL-3 20 | Encoding: UTF-8 21 | Roxygen: list(markdown = TRUE) 22 | RoxygenNote: 7.3.2 23 | Depends: 24 | R (>= 2.10) 25 | Imports: 26 | box (>= 1.1.3), 27 | box.linters (>= 0.10.5), 28 | box.lsp, 29 | callr, 30 | cli, 31 | config, 32 | fs, 33 | glue, 34 | lintr (>= 3.0.0), 35 | logger, 36 | purrr, 37 | renv, 38 | rstudioapi, 39 | sass, 40 | shiny, 41 | styler, 42 | testthat (>= 3.0.0), 43 | utils, 44 | withr, 45 | yaml 46 | Suggests: 47 | covr, 48 | knitr, 49 | lifecycle, 50 | mockery, 51 | rcmdcheck, 52 | rex, 53 | rlang, 54 | rmarkdown, 55 | shiny.react, 56 | spelling 57 | LazyData: true 58 | Config/testthat/edition: 3 59 | Config/testthat/parallel: true 60 | Language: en-US 61 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export("%<-%") 4 | export(app) 5 | export(auto_test_r) 6 | export(build_js) 7 | export(build_sass) 8 | export(devmode) 9 | export(diagnostics) 10 | export(format_js) 11 | export(format_r) 12 | export(format_sass) 13 | export(init) 14 | export(lint_js) 15 | export(lint_r) 16 | export(lint_sass) 17 | export(log) 18 | export(pkg_install) 19 | export(pkg_remove) 20 | export(react_component) 21 | export(test_e2e) 22 | export(test_r) 23 | import(box.linters) 24 | import(box.lsp) 25 | -------------------------------------------------------------------------------- /R/addins.R: -------------------------------------------------------------------------------- 1 | run_background <- function(file) { 2 | path <- fs::path_package("rhino", "rstudio", "addins", file) 3 | rstudioapi::jobRunScript(path, workingDir = rstudioapi::getActiveProject()) 4 | } 5 | 6 | addin_module <- function() { 7 | module_path <- fs::path_package("rhino", "rstudio", "addins", "module.R") 8 | file_path <- rstudioapi::selectFile( 9 | caption = "Module name and location", 10 | path = fs::path("app", "view"), 11 | filter = ".R", 12 | label = "Save", 13 | existing = FALSE 14 | ) 15 | if (is.null(file_path)) { 16 | message("Module creation canceled.") 17 | return() 18 | } 19 | 20 | if (tools::file_ext(file_path) == "") { 21 | file_path <- glue::glue("{file_path}.R") 22 | 23 | if (fs::file_exists(file_path)) { 24 | overwrite <- rstudioapi::showQuestion( 25 | title = "File already exists", 26 | message = "Would you like to overwrite it?", 27 | ok = "Yes", 28 | cancel = "No" 29 | ) 30 | 31 | if (!overwrite) { 32 | message("Module creation canceled.") 33 | return() 34 | } 35 | } 36 | } 37 | 38 | fs::file_copy(module_path, file_path, overwrite = TRUE) 39 | fs::file_show(file_path) 40 | } 41 | 42 | addin_format_r <- function() { 43 | selected_code <- rstudioapi::getActiveDocumentContext()$path 44 | if (selected_code != "") { 45 | rhino::format_r(selected_code) 46 | } else { 47 | path <- rstudioapi::selectFile( 48 | caption = "Select R script", 49 | filter = "(*.R)", 50 | existing = TRUE 51 | ) 52 | rhino::format_r(path) 53 | } 54 | } 55 | 56 | addin_lint_r <- function() { 57 | run_background("lint_r.R") 58 | } 59 | 60 | addin_test_r <- function() { 61 | run_background("test_r.R") 62 | } 63 | 64 | addin_build_js <- function() { 65 | type <- rstudioapi::showQuestion( 66 | title = "Watch argument", 67 | message = "Keep the process running and rebuilding Javascript whenever source files change?", 68 | ok = "Yes", 69 | cancel = "No" 70 | ) 71 | if (type) { 72 | run_background("build_js_watch.R") 73 | } else { 74 | run_background("build_js.R") 75 | } 76 | } 77 | 78 | addin_build_sass <- function() { 79 | type <- rstudioapi::showQuestion( 80 | title = "Watch argument", 81 | message = "Keep the process running and rebuilding Sass whenever source files change?", 82 | ok = "Yes", 83 | cancel = "No" 84 | ) 85 | if (type) { 86 | run_background("build_sass_watch.R") 87 | } else { 88 | run_background("build_sass.R") 89 | } 90 | } 91 | 92 | addin_lint_js <- function() { 93 | type <- rstudioapi::showQuestion( 94 | title = "Fix automatically?", 95 | message = "Would you like to automatically fix problems?", 96 | ok = "Yes", 97 | cancel = "No" 98 | ) 99 | if (type) { 100 | run_background("lint_js_fix.R") 101 | } else { 102 | run_background("lint_js.R") 103 | } 104 | } 105 | 106 | addin_lint_sass <- function() { 107 | type <- rstudioapi::showQuestion( 108 | title = "Fix automatically?", 109 | message = "Would you like to automatically fix problems?", 110 | ok = "Yes", 111 | cancel = "No" 112 | ) 113 | if (type) { 114 | run_background("lint_sass_fix.R") 115 | } else { 116 | run_background("lint_sass.R") 117 | } 118 | } 119 | 120 | addin_test_e2e <- function() { 121 | type <- rstudioapi::showQuestion( 122 | title = "Run interactive mode?", 123 | message = "Should Cypress be run in the interactive mode?", 124 | ok = "Yes", 125 | cancel = "No" 126 | ) 127 | if (type) { 128 | run_background("test_e2e_int.R") 129 | } else { 130 | run_background("test_e2e.R") 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /R/config.R: -------------------------------------------------------------------------------- 1 | # Given a path without extension, read either `{path}.yml` or `{path}.yaml`. 2 | read_yaml <- function(path) { 3 | yml <- paste0(path, ".yml") 4 | yaml <- paste0(path, ".yaml") 5 | if (fs::file_exists(yml)) { 6 | if (fs::file_exists(yaml)) { 7 | cli::cli_alert_warning("Both '{yml}' and '{yaml}' found; reading '{yml}'.") 8 | } 9 | yaml::read_yaml(yml) 10 | } else if (fs::file_exists(yaml)) { 11 | yaml::read_yaml(yaml) 12 | } else { 13 | cli::cli_abort("Neither '{yml}' nor '{yaml}' found.") 14 | } 15 | } 16 | 17 | option_validator <- function(...) { 18 | list( 19 | check = function(value) value %in% c(...), 20 | help = cli::format_inline("Allowed values: {c(...)}.") 21 | ) 22 | } 23 | 24 | positive_integer_validator <- list( 25 | check = function(value) is.integer(value) && value > 0, 26 | help = "Expected positive integer." 27 | ) 28 | 29 | rhino_config_definition <- list( 30 | list( 31 | name = "sass", 32 | validator = option_validator("node", "r", "custom"), 33 | required = TRUE 34 | ), 35 | list( 36 | name = "legacy_entrypoint", 37 | validator = option_validator("app_dir", "source", "box_top_level"), 38 | required = FALSE 39 | ), 40 | list( 41 | name = "legacy_max_lint_r_errors", 42 | validator = positive_integer_validator, 43 | required = FALSE 44 | ) 45 | ) 46 | 47 | validate_config <- function(definition, config) { 48 | if (is.null(config)) config <- list() 49 | if (!is.list(config)) { 50 | cli::cli_abort(c( 51 | "Config should be a named list (a YAML object).", 52 | i = "The received config has class {.cls {class(config)}}." 53 | )) 54 | } 55 | 56 | known_fields <- purrr::map_chr(definition, `[[`, "name") 57 | for (field in names(config)) { 58 | if (!(field %in% known_fields)) { 59 | cli::cli_abort("Unknown config field '{field}'.") 60 | } 61 | } 62 | 63 | for (field in definition) { 64 | if (field$name %in% names(config)) { 65 | value <- config[[field$name]] 66 | if (!field$validator$check(value)) { 67 | cli::cli_abort(c( 68 | "Invalid value '{value}' for field '{field$name}'.", 69 | i = field$validator$help 70 | )) 71 | } 72 | } else if (field$required) { 73 | cli::cli_abort("Missing required field '{field$name}'.") 74 | } 75 | } 76 | } 77 | 78 | read_config <- function() { 79 | config <- read_yaml("rhino") 80 | validate_config(rhino_config_definition, config) 81 | config 82 | } 83 | -------------------------------------------------------------------------------- /R/data.R: -------------------------------------------------------------------------------- 1 | #' Population of rhinos 2 | #' 3 | #' A dataset containing population of 5 species of rhinos. 4 | #' 5 | #' @format A data frame with 58 rows and 3 variables: 6 | #' \describe{ 7 | #' \item{Year}{year} 8 | #' \item{Population}{rhinos population} 9 | #' \item{Species}{rhinos species} 10 | #' } 11 | #' @source \url{https://ourworldindata.org/} 12 | "rhinos" 13 | -------------------------------------------------------------------------------- /R/dependencies.R: -------------------------------------------------------------------------------- 1 | read_dependencies <- function() { 2 | if (fs::file_exists("dependencies.R")) { 3 | cli::cli_alert_info("Reading '{.file dependencies.R}.'") 4 | renv::dependencies("dependencies.R")$Package 5 | } else if (fs::dir_exists("app")) { 6 | cli::cli_alert_info("Inferring dependencies from the {.file app} directory.") 7 | renv::dependencies("app")$Package 8 | } else { 9 | # It seems we are initializing a fresh project. 10 | character() 11 | } 12 | } 13 | 14 | write_dependencies <- function(deps) { 15 | if (as.numeric(R.Version()$major) >= 4 && as.numeric(R.Version()$minor) >= 3.0) { 16 | deps <- c(deps, "treesitter", "treesitter.r") 17 | } 18 | deps <- sort(unique(c("rhino", deps))) # Rhino is always needed as a dependency. 19 | deps <- purrr::map_chr(deps, function(name) glue::glue("library({name})")) 20 | deps <- c( 21 | "# This file allows packrat (used by rsconnect during deployment) to pick up dependencies.", 22 | deps 23 | ) 24 | writeLines(deps, "dependencies.R") 25 | } 26 | 27 | extract_package_name <- function(package) { 28 | if (grepl("@", package)) package <- strsplit(package, "@")[[1]][1] 29 | 30 | if (grepl("bioc::", package)) return(strsplit(package, "::")[[1]][2]) 31 | 32 | if (grepl("/", package)) { 33 | package_splited <- strsplit(package, "/")[[1]] 34 | return(package_splited[length(package_splited)]) 35 | } 36 | 37 | package 38 | } 39 | 40 | extract_packages_names <- function(packages) { 41 | purrr::map_chr(packages, extract_package_name) 42 | } 43 | 44 | # nolint start: line_length_linter 45 | #' Manage dependencies 46 | #' 47 | #' Install, remove or update the R package dependencies of your Rhino project. 48 | #' 49 | #' Use `pkg_install()` to install or update a package to the latest version. 50 | #' Use `pkg_remove()` to remove a package. 51 | #' 52 | #' These functions will install or remove packages from the local `{renv}` library, 53 | #' and update the `dependencies.R` and `renv.lock` files accordingly, all in one step. 54 | #' The underlying `{renv}` functions can still be called directly for advanced use cases. 55 | #' See the [Explanation: Renv configuration](https://appsilon.github.io/rhino/articles/explanation/renv-configuration.html) 56 | #' to learn about the details of the setup used by Rhino. 57 | #' 58 | #' @param packages Character vector of package names. 59 | #' @return None. This functions are called for side effects. 60 | #' @name dependencies 61 | #' 62 | #' @examples 63 | #' \dontrun{ 64 | #' # Install dplyr 65 | #' rhino::pkg_install("dplyr") 66 | #' 67 | #' # Update shiny to the latest version 68 | #' rhino::pkg_install("shiny") 69 | #' 70 | #' # Install a specific version of shiny 71 | #' rhino::pkg_install("shiny@1.6.0") 72 | #' 73 | #' # Install shiny.i18n package from GitHub 74 | #' rhino::pkg_install("Appsilon/shiny.i18n") 75 | #' 76 | #' # Install Biobase package from Bioconductor 77 | #' rhino::pkg_install("bioc::Biobase") 78 | #' 79 | #' # Install shiny from local source 80 | #' rhino::pkg_install("~/path/to/shiny") 81 | #' 82 | #' # Remove dplyr 83 | #' rhino::pkg_remove("dplyr") 84 | #' } 85 | # nolint end 86 | NULL 87 | 88 | #' @rdname dependencies 89 | #' @export 90 | pkg_install <- function(packages) { 91 | stopifnot(is.character(packages)) 92 | packages_names <- extract_packages_names(packages) 93 | cli::cli_alert_info("Installing packages: {packages_names}.") 94 | renv::install(packages) 95 | write_dependencies(c(packages_names, read_dependencies())) 96 | renv::snapshot() 97 | invisible() 98 | } 99 | 100 | #' @rdname dependencies 101 | #' @export 102 | pkg_remove <- function(packages) { 103 | stopifnot(is.character(packages)) 104 | packages_names <- extract_packages_names(packages) 105 | cli::cli_alert_info("Removing packages: {packages_names}.") 106 | renv::remove(packages) 107 | write_dependencies(setdiff(read_dependencies(), packages_names)) 108 | renv::snapshot() 109 | invisible() 110 | } 111 | -------------------------------------------------------------------------------- /R/destructure.R: -------------------------------------------------------------------------------- 1 | #' Destructure a named list into individual variables 2 | #' 3 | #' @description 4 | #' `r lifecycle::badge("experimental")` 5 | #' 6 | #' The destructuring operator `%<-%` allows you to extract multiple named values from a list 7 | #' into individual variables in a single assignment. This provides a convenient way to 8 | #' unpack list elements by name. 9 | #' 10 | #' While it works with any named list, it was primarily designed to improve the ergonomics 11 | #' of working with Shiny modules that return multiple reactive values. Instead of manually 12 | #' assigning each reactive value from a module's return list, you can destructure them all 13 | #' at once. 14 | #' 15 | #' @param lhs A call to `c()` containing variable names to assign to. All variable names should 16 | #' exist in the rhs list. 17 | #' @param rhs A named list containing the values to assign 18 | #' 19 | #' @return Invisibly returns the right-hand side list 20 | #' 21 | #' @examples 22 | #' # Basic destructuring 23 | #' data <- list(x = 1, y = 2, z = 3) 24 | #' c(x, y) %<-% data 25 | #' x # 1 26 | #' y # 2 27 | #' 28 | #' # Works with unsorted names 29 | #' result <- list(last = "Smith", first = "John") 30 | #' c(first, last) %<-% result 31 | #' 32 | #' # Shiny module example 33 | #' if (interactive()) { 34 | #' module_server <- function(id) { 35 | #' shiny::moduleServer(id, function(input, output, session) { 36 | #' list( 37 | #' value = shiny::reactive(input$num), 38 | #' text = shiny::reactive(input$txt) 39 | #' ) 40 | #' }) 41 | #' } 42 | #' 43 | #' # Clean extraction of reactive values 44 | #' c(value, text) %<-% module_server("my_module") 45 | #' } 46 | #' 47 | #' # Can be used with pipe operations 48 | #' # Note: The piped expression must be wrapped in brackets 49 | #' \dontrun{ 50 | #' c(value) %<-% ( 51 | #' 123 |> 52 | #' list(value = _) 53 | #' ) 54 | #' } 55 | #' @export 56 | `%<-%` <- function(lhs, rhs) { 57 | # LHS validation 58 | lhs <- substitute(lhs) 59 | if (!is.call(lhs)) { 60 | stop("%<-% : only calls are allowed on the left-hand side") 61 | } 62 | 63 | if (lhs[[1]] != as.name("c")) { 64 | stop("%<-% : invalid call on the left-hand side - only c() is allowed") 65 | } 66 | 67 | # RHS validation 68 | if (!is.list(rhs)) { 69 | stop("%<-% : only lists are allowed on the right-hand side") 70 | } 71 | 72 | if (is.data.frame(rhs)) { 73 | stop("%<-% : data.frames are not supported on the right-hand side") 74 | } 75 | 76 | if (is.null(names(rhs))) { 77 | stop("%<-% : only *named* lists are supported on the right-hand side") 78 | } 79 | 80 | lhs_names <- as.list(lhs)[-1] 81 | for (name in lhs_names) { 82 | name <- as.character(name) 83 | if (name == "...") { 84 | stop("%<-% : ... is not supported on the left-hand side") 85 | } 86 | 87 | value <- rhs[[name]] 88 | if (is.null(value)) { 89 | stop(paste0("%<-% : couldn't find the '", name, "' key in the list on the right-hand side")) 90 | } 91 | 92 | assign(name, value, envir = parent.frame()) 93 | } 94 | 95 | invisible(rhs) 96 | } 97 | -------------------------------------------------------------------------------- /R/init.R: -------------------------------------------------------------------------------- 1 | #' Create Rhino application 2 | #' 3 | #' Generates the file structure of a Rhino application. 4 | #' Can be used to start a fresh project or to migrate an existing Shiny application 5 | #' created without Rhino. 6 | #' 7 | #' The recommended steps for migrating an existing Shiny application to Rhino: 8 | #' 1. Put all app files in the `app` directory, 9 | #' so that it can be run with `shiny::shinyAppDir("app")` (assuming all dependencies are installed). 10 | #' 2. If you have a list of dependencies in form of `library()` calls, 11 | #' put them in the `dependencies.R` file. 12 | #' If this file does not exist, Rhino will generate it based on `renv::dependencies("app")`. 13 | #' 3. If your project uses `{renv}`, put `renv.lock` and `renv` directory in the project root. 14 | #' Rhino will try to only add the necessary dependencies to your lockfile. 15 | #' 4. Run `rhino::init()` in the project root. 16 | #' 17 | #' @param dir Name of the directory to create application in. 18 | #' @param github_actions_ci Should the GitHub Actions CI be added? 19 | #' @param rhino_version When using an existing `renv.lock` file, 20 | #' Rhino will install itself using `renv::install(rhino_version)`. 21 | #' You can provide this argument to use a specific version / source, e.g.`"Appsilon/rhino@v0.4.0"`. 22 | #' @param force Boolean; force initialization? 23 | #' By default, Rhino will refuse to initialize a project in the home directory. 24 | #' @return None. This function is called for side effects. 25 | #' 26 | #' @export 27 | init <- function( 28 | dir = ".", 29 | github_actions_ci = TRUE, 30 | rhino_version = "rhino", 31 | force = FALSE 32 | ) { 33 | is_home <- is_dir_home(dir = dir) 34 | 35 | if (!is_home || force) { 36 | init_impl( 37 | dir = dir, 38 | github_actions_ci = github_actions_ci, 39 | rhino_version = rhino_version, 40 | new_project_wizard = FALSE 41 | ) 42 | } else { 43 | cli::cli_abort( 44 | c( 45 | "Refusing to create a Rhino app in home directory {.path {dir}}!", 46 | i = "Set {.code force = TRUE} to force initialization." 47 | ), 48 | call = NULL 49 | ) 50 | } 51 | } 52 | 53 | init_rstudio <- function( 54 | dir = ".", 55 | github_actions_ci = TRUE, 56 | rhino_version = "rhino" 57 | ) { 58 | init_impl( 59 | # No need to check if `dir` is home, 60 | # because RStudio's new project wizard always creates a new directory. 61 | dir = dir, 62 | github_actions_ci = github_actions_ci, 63 | rhino_version = rhino_version, 64 | new_project_wizard = TRUE 65 | ) 66 | } 67 | 68 | init_impl <- function( 69 | dir, 70 | github_actions_ci, 71 | rhino_version, 72 | new_project_wizard 73 | ) { 74 | fs::dir_create(dir) 75 | withr::with_dir(dir, { 76 | create_rproj_file(new_project_wizard) 77 | init_renv(rhino_version) 78 | create_app_structure() 79 | create_unit_tests_structure() 80 | create_e2e_tests_structure() 81 | if (isTRUE(github_actions_ci)) add_github_actions_ci() 82 | }) 83 | } 84 | 85 | handle_old_rprofile <- function() { 86 | if (fs::file_exists(".Rprofile")) { 87 | cli::cli_alert_warning("Renaming existing '.Rprofile' to 'old.Rprofile'.") 88 | fs::file_move(".Rprofile", "old.Rprofile") 89 | } 90 | } 91 | 92 | init_renv <- function(rhino_version) { 93 | handle_old_rprofile() 94 | write_dependencies(read_dependencies()) 95 | copy_template("renv") 96 | if (fs::file_exists("renv.lock")) { 97 | renv::load() 98 | renv::restore(prompt = FALSE, clean = TRUE) 99 | renv::install(rhino_version) 100 | renv::snapshot() 101 | } else { 102 | # With `restart = TRUE`, RStudio fails to create a project 103 | # with an "Unable to establish connection with R session" message. 104 | renv::init(restart = FALSE) 105 | } 106 | cli::cli_alert_success("Initialized renv.") 107 | } 108 | 109 | create_rproj_file <- function(new_project_wizard) { 110 | if (!new_project_wizard && !rproj_exists()) { 111 | copy_rproj() 112 | cli::cli_alert_success("Rproj file created.") 113 | } 114 | } 115 | 116 | create_app_structure <- function() { 117 | copy_template("app_structure") 118 | cli::cli_alert_success("Application structure created.") 119 | } 120 | 121 | add_github_actions_ci <- function() { 122 | copy_template("github_ci") 123 | cli::cli_alert_success("Github Actions CI added.") 124 | } 125 | 126 | create_unit_tests_structure <- function() { 127 | copy_template("unit_tests") 128 | cli::cli_alert_success("Unit tests structure created.") 129 | } 130 | 131 | create_e2e_tests_structure <- function() { 132 | copy_template("e2e_tests") 133 | cli::cli_alert_success("E2E tests structure created.") 134 | } 135 | 136 | is_dir_home <- function(dir) { 137 | # For Windows, home by default should be C:\Users\user\Documents. 138 | # For Unix, home by default should be /home/user. 139 | home_path <- normalizePath("~") 140 | dir_path <- normalizePath(dir, mustWork = FALSE) 141 | dir_path == home_path 142 | } 143 | -------------------------------------------------------------------------------- /R/linters.R: -------------------------------------------------------------------------------- 1 | # R CMD Check "notes" that all declared Imports in DESCRIPTION should be used. box.linters is not 2 | # used by the rhino package itself, but by the initialized rhino app. There are no calls to 3 | # box.linters in the R/ or tests/ folders. box.linters is called in the rhino app dot.lintr 4 | # template in inst/templates/app_structure. The following lines manually imports box.linters making 5 | # R CMD Check happy. 6 | #' @import box.linters 7 | NULL 8 | 9 | # box.lsp is not used in the rhino package. It is used by a rhino app. Need to add this here 10 | # to tell `R CMD Check` or `devtools::check()` we use `box.lsp` 11 | #' @import box.lsp 12 | NULL 13 | -------------------------------------------------------------------------------- /R/log.R: -------------------------------------------------------------------------------- 1 | #' Logging functions 2 | #' 3 | #' Convenient way to log messages at a desired severity level. 4 | #' 5 | #' The `log` object is a list of logging functions, in order of decreasing severity: 6 | #' 1. `fatal` 7 | #' 2. `error` 8 | #' 3. `warn` 9 | #' 4. `success` 10 | #' 5. `info` 11 | #' 6. `debug` 12 | #' 7. `trace` 13 | #' 14 | #' Rhino configures logging based on settings read from the `config.yml` file 15 | #' in the root of your project: 16 | #' 1. `rhino_log_level`: The minimum severity of messages to be logged. 17 | #' 2. `rhino_log_file`: The file to save logs to. If `NA`, standard error stream will be used. 18 | #' 19 | #' The default `config.yml` file uses `!expr Sys.getenv()` 20 | #' so that log level and file can also be configured 21 | #' by setting the `RHINO_LOG_LEVEL` and `RHINO_LOG_FILE` environment variables. 22 | #' 23 | #' The functions re-exported by the `log` object are aliases for `{logger}` functions. 24 | #' You can also import the package and use it directly to utilize its full capabilities. 25 | #' 26 | #' @examples 27 | #' \dontrun{ 28 | #' box::use(rhino[log]) 29 | #' 30 | #' # Messages can be formatted using glue syntax. 31 | #' name <- "Rhino" 32 | #' log$warn("Hello {name}!") 33 | #' log$info("{1:3} + {1:3} = {2 * (1:3)}") 34 | #' } 35 | #' @export 36 | log <- list( 37 | fatal = logger::log_fatal, 38 | error = logger::log_error, 39 | warn = logger::log_warn, 40 | success = logger::log_success, 41 | info = logger::log_info, 42 | debug = logger::log_debug, 43 | trace = logger::log_trace 44 | ) 45 | -------------------------------------------------------------------------------- /R/node.R: -------------------------------------------------------------------------------- 1 | node_path <- function(...) { 2 | fs::path(".rhino", ...) 3 | } 4 | 5 | node_check_and_init <- function(npm_command) { 6 | check_system_dependency( 7 | cmd = npm_command, 8 | dependency_name = ifelse(npm_command == "npm", "Node.js", npm_command), 9 | documentation_url = "https://go.appsilon.com/rhino-system-dependencies" 10 | ) 11 | node_init(npm_command) 12 | } 13 | 14 | # Run `npm` or an alternative command specified by `RHINO_NPM`. 15 | # If needed, copy over Node.js template and install dependencies. 16 | npm <- function(...) { 17 | npm_command <- Sys.getenv("RHINO_NPM", "npm") 18 | node_check_and_init(npm_command) 19 | node_run(npm_command, ...) 20 | } 21 | 22 | node_init <- function(npm_command) { 23 | if (!fs::dir_exists(node_path())) { 24 | cli::cli_alert_info("Initializing Node.js directory...") 25 | copy_template("node", node_path()) 26 | } 27 | if (!fs::dir_exists(node_path("node_modules"))) { 28 | cli::cli_alert_info("Installing Node.js packages with {npm_command}...") 29 | node_run(npm_command, "install", "--no-audit", "--no-fund") 30 | } 31 | } 32 | 33 | # Run the specified command in Node.js directory (assume it already exists). 34 | node_run <- function(command, ..., status_ok = 0) { 35 | withr::with_dir(node_path(), { 36 | status <- system2(command = command, args = c(...)) 37 | }) 38 | if (status != status_ok) { 39 | cli::cli_abort("System command '{command}' exited with status {status}.") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /R/react.R: -------------------------------------------------------------------------------- 1 | shiny_react_available <- function() { 2 | requireNamespace("shiny.react", quietly = TRUE) 3 | } 4 | 5 | react_support <- function() { 6 | if (shiny_react_available()) { 7 | shiny::tagList( 8 | shiny.react::reactDependency(), 9 | shiny.react::shinyReactDependency(), 10 | shiny::tags$script(shiny::HTML(" 11 | window.Rhino = { 12 | registerReactComponents: (components) => { 13 | window.jsmodule.RhinoReact ??= {}; 14 | Object.assign(window.jsmodule.RhinoReact, components); 15 | }, 16 | }; 17 | ")) 18 | ) 19 | } 20 | } 21 | 22 | # nolint start: line_length_linter 23 | #' React components 24 | #' 25 | #' Declare the React components defined in your app. 26 | #' 27 | #' There are three steps to add a React component to your Rhino application: 28 | #' 1. Define the component using JSX and register it with `Rhino.registerReactComponents()`. 29 | #' 2. Declare the component in R with `rhino::react_component()`. 30 | #' 3. Use the component in your application. 31 | #' 32 | #' Please refer to the [Tutorial: Use React in Rhino](https://appsilon.github.io/rhino/articles/tutorial/use-react-in-rhino.html) 33 | #' to learn about the details. 34 | #' 35 | #' @param name The name of the component. 36 | #' @return A function representing the component. 37 | #' 38 | #' @examples 39 | #' # Declare the component. 40 | #' TextBox <- react_component("TextBox") 41 | #' 42 | #' # Use the component. 43 | #' ui <- TextBox("Hello!", font_size = 20) 44 | #' @export 45 | # nolint end 46 | react_component <- function(name) { 47 | if (!shiny_react_available()) { 48 | cli::cli_abort( 49 | "To use React components in your app, please add {.pkg shiny.react} to dependencies." 50 | ) 51 | } 52 | function(...) { 53 | shiny.react::reactElement( 54 | module = "RhinoReact", 55 | name = name, 56 | props = shiny.react::asProps(...) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /R/rhino.R: -------------------------------------------------------------------------------- 1 | rename_template_path <- function(path) { 2 | path <- fs::path_split(path)[[1]] 3 | path <- sub("^dot\\.", ".", path) 4 | path <- sub("\\.template$", "", path) 5 | fs::path_join(path) 6 | } 7 | 8 | # Copy template from source path (relative to `inst/templates`) to destination 9 | # with some renaming applied to the names of files and directories: 10 | # 1. Leading `dot.` is replaced with `.`. 11 | # 2. Trailing `.template` is removed. 12 | copy_template <- function(src, dst = ".") { 13 | src <- fs::path_package("rhino", "templates", src) 14 | target <- function(path) { 15 | path <- fs::path_rel(path, start = src) 16 | path <- rename_template_path(path) 17 | fs::path(dst, path) 18 | } 19 | 20 | fs::dir_create(dst) 21 | fs::dir_walk( 22 | path = src, 23 | recurse = TRUE, 24 | type = "directory", 25 | fun = function(dir) fs::dir_create(target(dir)) 26 | ) 27 | fs::dir_walk( 28 | path = src, 29 | recurse = TRUE, 30 | type = "file", 31 | fun = function(file) fs::file_copy(file, target(file)) 32 | ) 33 | } 34 | 35 | rproj_exists <- function() { 36 | length(fs::dir_ls(type = "file", glob = "*.Rproj")) > 0 37 | } 38 | 39 | copy_rproj <- function() { 40 | file_name <- paste0( 41 | basename(fs::path_abs(".")), 42 | ".Rproj" 43 | ) 44 | 45 | fs::file_copy( 46 | fs::path_package("rhino", "templates", "rproj", "Rproj.template"), 47 | fs::path(".", file_name) 48 | ) 49 | } 50 | 51 | system_cmd_version <- function(cmd, throw_error = FALSE) { 52 | tryCatch( 53 | system2(cmd, "--version", stdout = TRUE, stderr = TRUE), 54 | error = function(e) { 55 | if (isTRUE(throw_error)) cli::cli_abort(e) 56 | 57 | e$message 58 | } 59 | ) 60 | } 61 | 62 | check_system_dependency <- function( 63 | cmd, 64 | dependency_name, 65 | documentation_url, 66 | additional_message = NULL 67 | ) { 68 | message <- c( 69 | glue::glue("Do you have {dependency_name} installed?"), 70 | glue::glue("Check {documentation_url} for details."), 71 | additional_message 72 | ) 73 | 74 | tryCatch( 75 | system_cmd_version(cmd, TRUE), 76 | error = function(e) cli::cli_abort(message) 77 | ) 78 | } 79 | 80 | #' Print diagnostics 81 | #' 82 | #' Prints information which can be useful for diagnosing issues with Rhino. 83 | #' 84 | #' @return None. This function is called for side effects. 85 | #' 86 | #' @examples 87 | #' if (interactive()) { 88 | #' # Print diagnostic information. 89 | #' diagnostics() 90 | #' } 91 | #' @export 92 | diagnostics <- function() { 93 | writeLines(c( 94 | paste(Sys.info()[c("sysname", "release", "version")], collapse = " "), 95 | R.version.string, 96 | paste("rhino:", utils::packageVersion("rhino")), 97 | paste("node:", system_cmd_version("node")) 98 | )) 99 | } 100 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | # rhino 1.10.1 2 | 3 | This release contains a fix for issue affecting the CI and building Docker images with `shiny` applications based on `rhino`. Because of that, we kindly request the reviewer to consider an exception to the CRAN publishing frequency policy. 4 | 5 | ## R CMD check results 6 | 7 | 0 errors | 0 warnings | 1 note 8 | 9 | * This is a new release. 10 | -------------------------------------------------------------------------------- /data/rhinos.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/data/rhinos.rda -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | Addin 2 | Addins 3 | Appsilon 4 | Bugfix 5 | Destructure 6 | ESLint 7 | Init 8 | JS 9 | JSON 10 | JSX 11 | LLM 12 | LatinR 13 | LibSass 14 | Pharmaverse 15 | PowerShell 16 | RStudio 17 | Renv 18 | Renviron 19 | Rhinoverse 20 | Rprofile 21 | Rstudio 22 | SDK 23 | Stylelint 24 | UI 25 | VSCode 26 | Webpack 27 | autoreload 28 | blogpost 29 | bslib 30 | conf 31 | config 32 | destructure 33 | destructuring 34 | dev 35 | devtools 36 | entrypoint 37 | favicon 38 | formatter 39 | fullstack 40 | golem 41 | io 42 | isolatable 43 | js 44 | linter 45 | linters 46 | lockfile 47 | minified 48 | minifies 49 | mixins 50 | modularization 51 | modularize 52 | modularized 53 | namespaced 54 | nodejs 55 | npm 56 | nvm 57 | overridable 58 | parsers 59 | pnpm 60 | preconfigured 61 | renv 62 | roxygen 63 | rstudio 64 | scalable 65 | scss 66 | shinyapps 67 | shinymanager 68 | shinytest 69 | theming 70 | tooltips 71 | ui 72 | unintuitive 73 | usethis 74 | webpack 75 | yml 76 | -------------------------------------------------------------------------------- /inst/rstudio/addins.dcf: -------------------------------------------------------------------------------- 1 | Name: Rhino module 2 | Description: Inserts Rhino module at your cursor position. 3 | Binding: addin_module 4 | Interactive: false 5 | 6 | Name: Format R code 7 | Description: Uses the {styler} package to automatically format R script. 8 | Binding: addin_format_r 9 | Interactive: false 10 | 11 | Name: Lint R Code 12 | Description: Uses the {lintr} package to check all R sources for style errors. 13 | Binding: addin_lint_r 14 | Interactive: false 15 | 16 | Name: Lint JavaScript 17 | Description: Runs ESLint on the JavaScript sources in the app/js directory 18 | Binding: addin_lint_js 19 | Interactive: false 20 | 21 | Name: Lint Sass 22 | Description: Runs Stylelint on the Sass sources in the app/styles directory. 23 | Binding: addin_lint_sass 24 | Interactive: false 25 | 26 | Name: Run R unit tests 27 | Description: Uses the {testhat} package to run all unit tests in tests/testthat directory. 28 | Binding: addin_test_r 29 | Interactive: false 30 | 31 | Name: Run Cypress end-to-end tests 32 | Description: Uses Cypress to run end-to-end tests defined in the tests/cypress directory. 33 | Binding: addin_test_e2e 34 | Interactive: false 35 | 36 | Name: Build JavaScript 37 | Description: Builds the app/js/index.js file into app/static/js/app.min.js 38 | Binding: addin_build_js 39 | Interactive: false 40 | 41 | Name: Build Sass 42 | Description: Builds the app/styles/main.scss file into app/static/css/app.min.css 43 | Binding: addin_build_sass 44 | Interactive: false -------------------------------------------------------------------------------- /inst/rstudio/addins/build_js.R: -------------------------------------------------------------------------------- 1 | rhino::build_js(watch = FALSE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/build_js_watch.R: -------------------------------------------------------------------------------- 1 | rhino::build_js(watch = TRUE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/build_sass.R: -------------------------------------------------------------------------------- 1 | rhino::build_sass(watch = FALSE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/build_sass_watch.R: -------------------------------------------------------------------------------- 1 | rhino::build_sass(watch = TRUE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/lint_js.R: -------------------------------------------------------------------------------- 1 | rhino::lint_js(fix = FALSE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/lint_js_fix.R: -------------------------------------------------------------------------------- 1 | rhino::lint_js(fix = TRUE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/lint_r.R: -------------------------------------------------------------------------------- 1 | rhino::lint_r() 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/lint_sass.R: -------------------------------------------------------------------------------- 1 | rhino::lint_sass(fix = FALSE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/lint_sass_fix.R: -------------------------------------------------------------------------------- 1 | rhino::lint_sass(fix = TRUE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/module.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[moduleServer, NS] 3 | ) 4 | 5 | #' @export 6 | ui <- function(id) { 7 | ns <- NS(id) 8 | 9 | } 10 | 11 | #' @export 12 | server <- function(id) { 13 | moduleServer(id, function(input, output, session) { 14 | 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /inst/rstudio/addins/test_e2e.R: -------------------------------------------------------------------------------- 1 | rhino::test_e2e(interactive = FALSE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/test_e2e_int.R: -------------------------------------------------------------------------------- 1 | rhino::test_e2e(interactive = TRUE) 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/addins/test_r.R: -------------------------------------------------------------------------------- 1 | rhino::test_r() 2 | 3 | -------------------------------------------------------------------------------- /inst/rstudio/templates/project/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/inst/rstudio/templates/project/favicon.ico -------------------------------------------------------------------------------- /inst/rstudio/templates/project/init.dcf: -------------------------------------------------------------------------------- 1 | Binding: init_rstudio 2 | Title: Shiny Application using rhino 3 | Icon: favicon.ico 4 | 5 | Parameter: github_actions_ci 6 | Widget: CheckboxInput 7 | Label: Github Actions CI 8 | Default: On 9 | -------------------------------------------------------------------------------- /inst/templates/app_structure/app.R: -------------------------------------------------------------------------------- 1 | # Rhino / shinyApp entrypoint. Do not edit. 2 | rhino::app() 3 | -------------------------------------------------------------------------------- /inst/templates/app_structure/app/js/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/inst/templates/app_structure/app/js/index.js -------------------------------------------------------------------------------- /inst/templates/app_structure/app/logic/__init__.R: -------------------------------------------------------------------------------- 1 | # Logic: application code independent from Shiny. 2 | # https://go.appsilon.com/rhino-project-structure 3 | -------------------------------------------------------------------------------- /inst/templates/app_structure/app/main.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[bootstrapPage, div, moduleServer, NS, renderUI, tags, uiOutput], 3 | ) 4 | 5 | #' @export 6 | ui <- function(id) { 7 | ns <- NS(id) 8 | bootstrapPage( 9 | uiOutput(ns("message")) 10 | ) 11 | } 12 | 13 | #' @export 14 | server <- function(id) { 15 | moduleServer(id, function(input, output, session) { 16 | output$message <- renderUI({ 17 | div( 18 | style = "display: flex; justify-content: center; align-items: center; height: 100vh;", 19 | tags$h1( 20 | tags$a("Check out Rhino docs!", href = "https://appsilon.github.io/rhino/") 21 | ) 22 | ) 23 | }) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /inst/templates/app_structure/app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/inst/templates/app_structure/app/static/favicon.ico -------------------------------------------------------------------------------- /inst/templates/app_structure/app/styles/main.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/inst/templates/app_structure/app/styles/main.scss -------------------------------------------------------------------------------- /inst/templates/app_structure/app/view/__init__.R: -------------------------------------------------------------------------------- 1 | # View: Shiny modules and related code. 2 | # https://go.appsilon.com/rhino-project-structure 3 | -------------------------------------------------------------------------------- /inst/templates/app_structure/config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | rhino_log_level: !expr Sys.getenv("RHINO_LOG_LEVEL", "INFO") 3 | rhino_log_file: !expr Sys.getenv("RHINO_LOG_FILE", NA) 4 | -------------------------------------------------------------------------------- /inst/templates/app_structure/dot.lintr: -------------------------------------------------------------------------------- 1 | linters: 2 | linters_with_defaults( 3 | defaults = box.linters::rhino_default_linters, 4 | line_length_linter = line_length_linter(100) 5 | ) 6 | -------------------------------------------------------------------------------- /inst/templates/app_structure/dot.rscignore: -------------------------------------------------------------------------------- 1 | .github 2 | .lintr 3 | .renvignore 4 | .Renviron 5 | .rhino 6 | .rscignore 7 | tests 8 | -------------------------------------------------------------------------------- /inst/templates/app_structure/rhino.yml: -------------------------------------------------------------------------------- 1 | sass: node 2 | -------------------------------------------------------------------------------- /inst/templates/e2e_tests/tests/cypress.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | e2e: { 3 | setupNodeEvents(on, config) {}, 4 | baseUrl: 'http://localhost:3333', 5 | supportFile: false, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /inst/templates/e2e_tests/tests/cypress/dot.gitignore: -------------------------------------------------------------------------------- 1 | /screenshots/ 2 | /videos/ 3 | -------------------------------------------------------------------------------- /inst/templates/e2e_tests/tests/cypress/e2e/app.cy.js: -------------------------------------------------------------------------------- 1 | describe('app', () => { 2 | beforeEach(() => { 3 | cy.visit('/') 4 | }) 5 | 6 | it('starts', () => {}) 7 | }) 8 | -------------------------------------------------------------------------------- /inst/templates/github_ci/dot.github/workflows/rhino-test.yml: -------------------------------------------------------------------------------- 1 | name: Rhino Test 2 | on: 3 | # Run on pushes to 'main' branch 4 | push: 5 | branches: 6 | - main 7 | # Run on any opened pull request 8 | pull_request: 9 | # Run manually via GitHub Actions website 10 | workflow_dispatch: 11 | permissions: 12 | contents: read 13 | jobs: 14 | main: 15 | name: Run linters and tests 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - name: Checkout repo 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup system dependencies 22 | run: | 23 | packages=( 24 | # List each package on a separate line. 25 | ) 26 | sudo apt-get update 27 | sudo apt-get install --yes "${packages[@]}" 28 | 29 | - name: Setup R 30 | uses: r-lib/actions/setup-r@v2 31 | with: 32 | r-version: renv 33 | 34 | - name: Setup R dependencies 35 | uses: r-lib/actions/setup-renv@v2 36 | 37 | - name: Setup Node 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: 20 41 | 42 | - name: Lint R 43 | if: always() 44 | shell: Rscript {0} 45 | run: rhino::lint_r() 46 | 47 | - name: Lint JavaScript 48 | if: always() 49 | shell: Rscript {0} 50 | run: rhino::lint_js() 51 | 52 | - name: Lint Sass 53 | if: always() 54 | shell: Rscript {0} 55 | run: rhino::lint_sass() 56 | 57 | - name: Build JavaScript 58 | if: always() 59 | shell: Rscript {0} 60 | run: rhino::build_js() 61 | 62 | - name: Build Sass 63 | if: always() 64 | shell: Rscript {0} 65 | run: rhino::build_sass() 66 | 67 | - name: Run R unit tests 68 | if: always() 69 | shell: Rscript {0} 70 | run: rhino::test_r() 71 | 72 | - name: Run Cypress end-to-end tests 73 | if: always() 74 | uses: cypress-io/github-action@v6 75 | with: 76 | working-directory: .rhino # Created by earlier commands which use Node.js 77 | start: npm run run-app 78 | project: ../tests 79 | wait-on: 'http://localhost:3333/' 80 | wait-on-timeout: 60 81 | -------------------------------------------------------------------------------- /inst/templates/node/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /inst/templates/node/dot.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "extends": ["airbnb"], 4 | "env": { 5 | "browser": true 6 | }, 7 | "globals": { 8 | "$": "readonly", 9 | "Rhino": "readonly", 10 | "Shiny": "readonly" 11 | }, 12 | "rules": { 13 | "import/prefer-default-export": "off", 14 | "no-alert": "off", 15 | "no-console": "off" 16 | }, 17 | "settings": { 18 | "react": { 19 | "version": "17.0" // React is attached by shiny.react, so its version cannot be detected. 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /inst/templates/node/dot.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /inst/templates/node/dot.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard-scss", 3 | "rules": { 4 | "at-rule-no-unknown": null, 5 | "no-empty-source": null, 6 | "scss/at-rule-no-unknown": true, 7 | "selector-id-pattern": [ 8 | "^([a-z][a-z0-9]*)([-_][a-z0-9]+)*$", 9 | { 10 | "message": "Expected ID selector to be kebab-snake_case" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /inst/templates/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build-js": "webpack", 5 | "build-sass": "sass --no-source-map --style=compressed ../app/styles/main.scss:../app/static/css/app.min.css", 6 | "lint-js": "eslint --config .eslintrc.json ../app/js", 7 | "lint-sass": "stylelint ../app/styles", 8 | "format-js": "prettier --config prettier.config.mjs --ignore-path none ../app/js/**/*.js", 9 | "format-sass": "prettier --config prettier.config.mjs --ignore-path none ../app/styles/**/*.scss", 10 | "run-app": "cd .. && Rscript -e \"shiny::runApp(port = 3333)\"", 11 | "run-cypress": "cypress run --project ../tests", 12 | "open-cypress": "cypress open --project ../tests", 13 | "test-e2e": "start-server-and-test run-app http://localhost:3333 run-cypress", 14 | "test-e2e-interactive": "start-server-and-test run-app http://localhost:3333 open-cypress" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.23.7", 18 | "@babel/eslint-parser": "^7.23.3", 19 | "@babel/preset-env": "^7.23.7", 20 | "@babel/preset-react": "^7.23.3", 21 | "babel-loader": "^9.1.3", 22 | "cypress": "^13.6.2", 23 | "eslint": "^8.56.0", 24 | "eslint-config-airbnb": "^19.0.4", 25 | "eslint-import-resolver-webpack": "^0.13.8", 26 | "eslint-plugin-import": "^2.29.1", 27 | "prettier": "^3.3.2", 28 | "sass": "^1.69.7", 29 | "start-server-and-test": "^2.0.3", 30 | "stylelint": "^14.16.1", 31 | "stylelint-config-standard-scss": "^6.1.0", 32 | "webpack": "^5.89.0", 33 | "webpack-cli": "^5.1.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /inst/templates/node/prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | export default { 3 | overrides: [ 4 | { 5 | files: "../app/js/**/*.js", 6 | options: { 7 | singleQuote: true, 8 | }, 9 | }, 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /inst/templates/node/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | const appDir = join(__dirname, '..', 'app'); 4 | 5 | module.exports = { 6 | mode: 'production', 7 | entry: join(appDir, 'js', 'index.js'), 8 | output: { 9 | library: 'App', 10 | path: join(appDir, 'static', 'js'), 11 | filename: 'app.min.js', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx)$/, 17 | use: 'babel-loader', 18 | }, 19 | ], 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.jsx'], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /inst/templates/renv/dot.Rprofile: -------------------------------------------------------------------------------- 1 | if (file.exists("renv")) { 2 | source("renv/activate.R") 3 | } else { 4 | # The `renv` directory is automatically skipped when deploying with rsconnect. 5 | message("No 'renv' directory found; renv won't be activated.") 6 | } 7 | 8 | # Allow absolute module imports (relative to the app root). 9 | options(box.path = getwd()) 10 | 11 | # box.lsp languageserver external hook 12 | if (nzchar(system.file(package = "box.lsp"))) { 13 | options( 14 | languageserver.parser_hooks = list( 15 | "box::use" = box.lsp::box_use_parser 16 | ) 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /inst/templates/renv/dot.renvignore: -------------------------------------------------------------------------------- 1 | # Only use `dependencies.R` to infer project dependencies. 2 | * 3 | !dependencies.R 4 | -------------------------------------------------------------------------------- /inst/templates/rproj/Rproj.template: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | -------------------------------------------------------------------------------- /inst/templates/unit_tests/tests/testthat/test-main.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[testServer], 3 | testthat[expect_true, test_that], 4 | ) 5 | box::use( 6 | app/main[server], 7 | ) 8 | 9 | test_that("main server works", { 10 | testServer(server, { 11 | expect_true(grepl(x = output$message$html, pattern = "Check out Rhino docs!")) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /man/app.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/app.R 3 | \name{app} 4 | \alias{app} 5 | \title{Rhino application} 6 | \usage{ 7 | app() 8 | } 9 | \value{ 10 | An object representing the app (can be passed to \code{shiny::runApp()}). 11 | } 12 | \description{ 13 | The entrypoint for a Rhino application. 14 | Your \code{app.R} should contain nothing but a call to \code{rhino::app()}. 15 | } 16 | \details{ 17 | This function is a wrapper around \code{shiny::shinyApp()}. 18 | It reads \code{rhino.yml} and performs some configuration steps (logger, static files, box modules). 19 | You can run a Rhino application in typical fashion using \code{shiny::runApp()}. 20 | 21 | Rhino will load the \code{app/main.R} file as a box module (\code{box::use(app/main)}). 22 | It should export two functions which take a single \code{id} argument - 23 | the \code{ui} and \code{server} of your top-level Shiny module. 24 | } 25 | \section{Legacy entrypoint}{ 26 | It is possible to specify a different way to load your application 27 | using the \code{legacy_entrypoint} option in \code{rhino.yml}: 28 | \enumerate{ 29 | \item \code{app_dir}: Rhino will run the app using \code{shiny::shinyAppDir("app")}. 30 | \item \code{source}: Rhino will \code{source("app/main.R")}. 31 | This file should define the top-level \code{ui} and \code{server} objects to be passed to \code{shinyApp()}. 32 | \item \code{box_top_level}: Rhino will load \code{app/main.R} as a box module (as it does by default), 33 | but the exported \code{ui} and \code{server} objects will be considered as top-level. 34 | } 35 | 36 | The \code{legacy_entrypoint} setting is useful when migrating an existing Shiny application to Rhino. 37 | It is recommended to transform your application step by step: 38 | \enumerate{ 39 | \item With \code{app_dir} you should be able to run your application right away 40 | (just put the files in the \code{app} directory). 41 | \item With \code{source} setting your application structure must be brought closer to Rhino, 42 | but you can still use \code{library()} and \code{source()} functions. 43 | \item With \code{box_top_level} you can be confident that the whole app is properly modularized, 44 | as box modules can only load other box modules (\code{library()} and \code{source()} won't work). 45 | \item The last step is to remove the \code{legacy_entrypoint} setting completely. 46 | Compared to \code{box_top_level} you'll need to make your top-level \code{ui} and \code{server} 47 | into a \href{https://shiny.rstudio.com/articles/modules.html}{Shiny module} 48 | (functions taking a single \code{id} argument). 49 | } 50 | } 51 | 52 | \examples{ 53 | \dontrun{ 54 | # Your `app.R` should contain nothing but this single call: 55 | rhino::app() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /man/auto_test_r.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{auto_test_r} 4 | \alias{auto_test_r} 5 | \title{Watch and automatically run R tests} 6 | \usage{ 7 | auto_test_r(reporter = NULL, filter = NULL, hash = TRUE) 8 | } 9 | \arguments{ 10 | \item{reporter}{\code{{testthat}} reporter to use. 11 | If NULL, will use \code{testthat::default_reporter()} for tests when running all tests 12 | and \code{testthat::default_compact_reporter()} for single file tests. 13 | See \href{https://testthat.r-lib.org/reference/Reporter.html}{\code{{testthat}} reporters} for more details.} 14 | 15 | \item{filter}{filter passed to \code{testthat::test_dir()}. If not NULL, only tests with file names matching this regular expression will be executed. 16 | Matching is performed on the file name after it's stripped of "test-" and ".R". 17 | Does not affect the case when a test file is changed. In this case, this test file is rerun.} 18 | 19 | \item{hash}{Logical. Whether to use file hashing to detect changes. Default is TRUE. 20 | If FALSE, file modification times are used instead.} 21 | } 22 | \value{ 23 | None. This function is called for side effects. 24 | } 25 | \description{ 26 | Watches R files in the \code{app} directory and \code{tests/testthat} directory for changes. 27 | When code files in \code{app} change, all tests are rerun. When test files change, 28 | only the changed test file is rerun. 29 | } 30 | \examples{ 31 | if (interactive()) { 32 | # Watch files and automatically run tests when changes are detected 33 | auto_test_r() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /man/build_js.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{build_js} 4 | \alias{build_js} 5 | \title{Build JavaScript} 6 | \usage{ 7 | build_js(watch = FALSE) 8 | } 9 | \arguments{ 10 | \item{watch}{Keep the process running and rebuilding JS whenever source files change.} 11 | } 12 | \value{ 13 | None. This function is called for side effects. 14 | } 15 | \description{ 16 | Builds the \code{app/js/index.js} file into \code{app/static/js/app.min.js}. 17 | The code is transformed and bundled 18 | using \href{https://babeljs.io}{Babel} and \href{https://webpack.js.org}{webpack}, 19 | so the latest JavaScript features can be used 20 | (including ECMAScript 2015 aka ES6 and newer standards). 21 | Requires Node.js to be available on the system. 22 | } 23 | \details{ 24 | Functions/objects defined in the global scope do not automatically become \code{window} properties, 25 | so the following JS code: 26 | 27 | \if{html}{\out{
}}\preformatted{function sayHello() \{ alert('Hello!'); \} 28 | }\if{html}{\out{
}} 29 | 30 | won't work as expected if used in R like this: 31 | 32 | \if{html}{\out{
}}\preformatted{tags$button("Hello!", onclick = 'sayHello()'); 33 | }\if{html}{\out{
}} 34 | 35 | Instead you should explicitly export functions: 36 | 37 | \if{html}{\out{
}}\preformatted{export function sayHello() \{ alert('Hello!'); \} 38 | }\if{html}{\out{
}} 39 | 40 | and access them via the global \code{App} object: 41 | 42 | \if{html}{\out{
}}\preformatted{tags$button("Hello!", onclick = "App.sayHello()") 43 | }\if{html}{\out{
}} 44 | } 45 | \examples{ 46 | if (interactive()) { 47 | # Build the `app/js/index.js` file into `app/static/js/app.min.js`. 48 | build_js() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /man/build_sass.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{build_sass} 4 | \alias{build_sass} 5 | \title{Build Sass} 6 | \usage{ 7 | build_sass(watch = FALSE) 8 | } 9 | \arguments{ 10 | \item{watch}{Keep the process running and rebuilding Sass whenever source files change. 11 | Only supported for \code{sass: node} configuration in \code{rhino.yml}.} 12 | } 13 | \value{ 14 | None. This function is called for side effects. 15 | } 16 | \description{ 17 | Builds the \code{app/styles/main.scss} file into \code{app/static/css/app.min.css}. 18 | } 19 | \details{ 20 | The build method can be configured using the \code{sass} option in \code{rhino.yml}: 21 | \enumerate{ 22 | \item \code{node}: Use \href{https://sass-lang.com/dart-sass}{Dart Sass} 23 | (requires Node.js to be available on the system). 24 | \item \code{r}: Use the \code{{sass}} R package. 25 | } 26 | 27 | It is recommended to use Dart Sass which is the primary, 28 | actively developed implementation of Sass. 29 | On systems without Node.js you can use the \code{{sass}} R package as a fallback. 30 | It is not advised however, as it uses the deprecated 31 | \href{https://sass-lang.com/blog/libsass-is-deprecated}{LibSass} implementation. 32 | } 33 | \examples{ 34 | if (interactive()) { 35 | # Build the `app/styles/main.scss` file into `app/static/css/app.min.css`. 36 | build_sass() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /man/dependencies.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/dependencies.R 3 | \name{dependencies} 4 | \alias{dependencies} 5 | \alias{pkg_install} 6 | \alias{pkg_remove} 7 | \title{Manage dependencies} 8 | \usage{ 9 | pkg_install(packages) 10 | 11 | pkg_remove(packages) 12 | } 13 | \arguments{ 14 | \item{packages}{Character vector of package names.} 15 | } 16 | \value{ 17 | None. This functions are called for side effects. 18 | } 19 | \description{ 20 | Install, remove or update the R package dependencies of your Rhino project. 21 | } 22 | \details{ 23 | Use \code{pkg_install()} to install or update a package to the latest version. 24 | Use \code{pkg_remove()} to remove a package. 25 | 26 | These functions will install or remove packages from the local \code{{renv}} library, 27 | and update the \code{dependencies.R} and \code{renv.lock} files accordingly, all in one step. 28 | The underlying \code{{renv}} functions can still be called directly for advanced use cases. 29 | See the \href{https://appsilon.github.io/rhino/articles/explanation/renv-configuration.html}{Explanation: Renv configuration} 30 | to learn about the details of the setup used by Rhino. 31 | } 32 | \examples{ 33 | \dontrun{ 34 | # Install dplyr 35 | rhino::pkg_install("dplyr") 36 | 37 | # Update shiny to the latest version 38 | rhino::pkg_install("shiny") 39 | 40 | # Install a specific version of shiny 41 | rhino::pkg_install("shiny@1.6.0") 42 | 43 | # Install shiny.i18n package from GitHub 44 | rhino::pkg_install("Appsilon/shiny.i18n") 45 | 46 | # Install Biobase package from Bioconductor 47 | rhino::pkg_install("bioc::Biobase") 48 | 49 | # Install shiny from local source 50 | rhino::pkg_install("~/path/to/shiny") 51 | 52 | # Remove dplyr 53 | rhino::pkg_remove("dplyr") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /man/devmode.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{devmode} 4 | \alias{devmode} 5 | \title{Development mode} 6 | \usage{ 7 | devmode( 8 | build_sass = TRUE, 9 | build_js = TRUE, 10 | run_r_unit_tests = TRUE, 11 | auto_test_r_args = list(reporter = NULL, filter = NULL, hash = TRUE), 12 | ... 13 | ) 14 | } 15 | \arguments{ 16 | \item{build_sass}{Boolean. Rebuild Sass automatically in the background?} 17 | 18 | \item{build_js}{Boolean. Rebuild JavaScript automatically in the background?} 19 | 20 | \item{run_r_unit_tests}{Boolean. Run R unit tests automatically in the background?} 21 | 22 | \item{auto_test_r_args}{List. Additional arguments passed to \code{auto_test_r()}.} 23 | 24 | \item{...}{Additional arguments passed to \code{shiny::runApp()}.} 25 | } 26 | \value{ 27 | None. This function is called for side effects. 28 | } 29 | \description{ 30 | Run application in development mode with automatic rebuilding and reloading. 31 | } 32 | \details{ 33 | This function will launch the Shiny app in 34 | \href{https://shiny.posit.co/r/reference/shiny/latest/devmode.html}{development mode} 35 | (as if \code{options(shiny.devmode = TRUE)} was set). 36 | The app will be automatically reloaded whenever the sources change. 37 | 38 | Additionally, Rhino will automatically rebuild JavaScript and Sass in the background 39 | and run R unit tests with the \code{auto_test_r()} function. 40 | Please note that this feature requires Node.js. 41 | } 42 | -------------------------------------------------------------------------------- /man/diagnostics.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rhino.R 3 | \name{diagnostics} 4 | \alias{diagnostics} 5 | \title{Print diagnostics} 6 | \usage{ 7 | diagnostics() 8 | } 9 | \value{ 10 | None. This function is called for side effects. 11 | } 12 | \description{ 13 | Prints information which can be useful for diagnosing issues with Rhino. 14 | } 15 | \examples{ 16 | if (interactive()) { 17 | # Print diagnostic information. 18 | diagnostics() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: deprecated 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | deprecated 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-experimental.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: experimental 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | experimental 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-stable.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: stable 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | lifecycle 21 | 22 | 25 | 26 | stable 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /man/figures/lifecycle-superseded.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: superseded 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | superseded 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/rhino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/man/figures/rhino.png -------------------------------------------------------------------------------- /man/format_js.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{format_js} 4 | \alias{format_js} 5 | \title{Format JavaScript} 6 | \usage{ 7 | format_js(fix = TRUE) 8 | } 9 | \arguments{ 10 | \item{fix}{If \code{TRUE}, fixes formatting. If FALSE, reports formatting errors without fixing them.} 11 | } 12 | \value{ 13 | None. This function is called for side effects. 14 | } 15 | \description{ 16 | Runs \href{https://prettier.io/}{prettier} on JavaScript files in \code{app/js} directory. 17 | Requires Node.js installed. 18 | } 19 | \details{ 20 | You can prevent prettier from formatting a given chunk of your code by adding a special comment: 21 | 22 | \if{html}{\out{
}}\preformatted{// prettier-ignore 23 | }\if{html}{\out{
}} 24 | 25 | Read more about \href{https://prettier.io/docs/en/ignore}{ignoring code}. 26 | } 27 | -------------------------------------------------------------------------------- /man/format_r.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{format_r} 4 | \alias{format_r} 5 | \title{Format R} 6 | \usage{ 7 | format_r(paths, exclude_files = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{paths}{Character vector of files and directories to format.} 11 | 12 | \item{exclude_files}{Character vector with regular expressions of files that should be excluded 13 | from styling.} 14 | 15 | \item{...}{Optional arguments to pass to \verb{box.linters::style_*} functions.} 16 | } 17 | \value{ 18 | None. This function is called for side effects. 19 | } 20 | \description{ 21 | Uses the \code{{styler}} and \code{{box.linters}} packages to automatically format R sources. As with 22 | \code{styler}, carefully examine the results after running this function. 23 | } 24 | \details{ 25 | The code is formatted according to the \code{styler::tidyverse_style} guide with one adjustment: 26 | spacing around math operators is not modified to avoid conflicts with \code{box::use()} statements. 27 | 28 | If available, \code{box::use()} calls are reformatted by styling functions provided by 29 | \code{{box.linters}}. These include: 30 | \itemize{ 31 | \item Separating \code{box::use()} calls for packages and local modules 32 | \item Alphabetically sorting packages, modules, and functions. 33 | \item Adding trailing commas 34 | } 35 | 36 | \verb{box.linters::style_*} functions require the \code{treesitter} and \code{treesitter.r} packages. These, in 37 | turn, require R >= 4.3.0. \code{format_r()} will continue to operate without these but will not 38 | perform \code{box::use()} call styling. 39 | 40 | For more information on \code{box::use()} call styling please refer to the \code{{box.linters}} styling 41 | functions 42 | \href{https://appsilon.github.io/box.linters/reference/style_box_use_text.html}{documentation}. 43 | } 44 | \examples{ 45 | if (interactive()) { 46 | # Format a single file. 47 | format_r("app/main.R") 48 | 49 | # Format all files in a directory. 50 | format_r("app/view") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /man/format_sass.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{format_sass} 4 | \alias{format_sass} 5 | \title{Format Sass} 6 | \usage{ 7 | format_sass(fix = TRUE) 8 | } 9 | \arguments{ 10 | \item{fix}{If \code{TRUE}, fixes formatting. If FALSE, reports formatting errors without fixing them.} 11 | } 12 | \value{ 13 | None. This function is called for side effects. 14 | } 15 | \description{ 16 | Runs \href{https://prettier.io/}{prettier} on Sass (.scss) files in \code{app/styles} directory. 17 | Requires Node.js installed. 18 | } 19 | \details{ 20 | You can prevent prettier from formatting a given chunk of your code by adding a special comment: 21 | 22 | \if{html}{\out{
}}\preformatted{// prettier-ignore 23 | }\if{html}{\out{
}} 24 | 25 | Read more about \href{https://prettier.io/docs/en/ignore}{ignoring code}. 26 | } 27 | -------------------------------------------------------------------------------- /man/grapes-set-grapes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/destructure.R 3 | \name{\%<-\%} 4 | \alias{\%<-\%} 5 | \title{Destructure a named list into individual variables} 6 | \usage{ 7 | lhs \%<-\% rhs 8 | } 9 | \arguments{ 10 | \item{lhs}{A call to \code{c()} containing variable names to assign to. All variable names should 11 | exist in the rhs list.} 12 | 13 | \item{rhs}{A named list containing the values to assign} 14 | } 15 | \value{ 16 | Invisibly returns the right-hand side list 17 | } 18 | \description{ 19 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 20 | 21 | The destructuring operator \verb{\%<-\%} allows you to extract multiple named values from a list 22 | into individual variables in a single assignment. This provides a convenient way to 23 | unpack list elements by name. 24 | 25 | While it works with any named list, it was primarily designed to improve the ergonomics 26 | of working with Shiny modules that return multiple reactive values. Instead of manually 27 | assigning each reactive value from a module's return list, you can destructure them all 28 | at once. 29 | } 30 | \examples{ 31 | # Basic destructuring 32 | data <- list(x = 1, y = 2, z = 3) 33 | c(x, y) \%<-\% data 34 | x # 1 35 | y # 2 36 | 37 | # Works with unsorted names 38 | result <- list(last = "Smith", first = "John") 39 | c(first, last) \%<-\% result 40 | 41 | # Shiny module example 42 | if (interactive()) { 43 | module_server <- function(id) { 44 | shiny::moduleServer(id, function(input, output, session) { 45 | list( 46 | value = shiny::reactive(input$num), 47 | text = shiny::reactive(input$txt) 48 | ) 49 | }) 50 | } 51 | 52 | # Clean extraction of reactive values 53 | c(value, text) \%<-\% module_server("my_module") 54 | } 55 | 56 | # Can be used with pipe operations 57 | # Note: The piped expression must be wrapped in brackets 58 | \dontrun{ 59 | c(value) \%<-\% ( 60 | 123 |> 61 | list(value = _) 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /man/init.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/init.R 3 | \name{init} 4 | \alias{init} 5 | \title{Create Rhino application} 6 | \usage{ 7 | init( 8 | dir = ".", 9 | github_actions_ci = TRUE, 10 | rhino_version = "rhino", 11 | force = FALSE 12 | ) 13 | } 14 | \arguments{ 15 | \item{dir}{Name of the directory to create application in.} 16 | 17 | \item{github_actions_ci}{Should the GitHub Actions CI be added?} 18 | 19 | \item{rhino_version}{When using an existing \code{renv.lock} file, 20 | Rhino will install itself using \code{renv::install(rhino_version)}. 21 | You can provide this argument to use a specific version / source, e.g.\code{"Appsilon/rhino@v0.4.0"}.} 22 | 23 | \item{force}{Boolean; force initialization? 24 | By default, Rhino will refuse to initialize a project in the home directory.} 25 | } 26 | \value{ 27 | None. This function is called for side effects. 28 | } 29 | \description{ 30 | Generates the file structure of a Rhino application. 31 | Can be used to start a fresh project or to migrate an existing Shiny application 32 | created without Rhino. 33 | } 34 | \details{ 35 | The recommended steps for migrating an existing Shiny application to Rhino: 36 | \enumerate{ 37 | \item Put all app files in the \code{app} directory, 38 | so that it can be run with \code{shiny::shinyAppDir("app")} (assuming all dependencies are installed). 39 | \item If you have a list of dependencies in form of \code{library()} calls, 40 | put them in the \code{dependencies.R} file. 41 | If this file does not exist, Rhino will generate it based on \code{renv::dependencies("app")}. 42 | \item If your project uses \code{{renv}}, put \code{renv.lock} and \code{renv} directory in the project root. 43 | Rhino will try to only add the necessary dependencies to your lockfile. 44 | \item Run \code{rhino::init()} in the project root. 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /man/lint_js.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{lint_js} 4 | \alias{lint_js} 5 | \title{Lint JavaScript} 6 | \usage{ 7 | lint_js(fix = FALSE) 8 | } 9 | \arguments{ 10 | \item{fix}{Automatically fix problems.} 11 | } 12 | \value{ 13 | None. This function is called for side effects. 14 | } 15 | \description{ 16 | Runs \href{https://eslint.org}{ESLint} on the JavaScript sources in the \code{app/js} directory. 17 | Requires Node.js to be available on the system. 18 | } 19 | \details{ 20 | If your JS code uses global objects defined by other JS libraries or R packages, 21 | you'll need to let the linter know or it will complain about undefined objects. 22 | For example, the \code{{leaflet}} package defines a global object \code{L}. 23 | To access it without raising linter errors, add \verb{/* global L */} comment in your JS code. 24 | 25 | You don't need to define \code{Shiny} and \code{$} as these global variables are defined by default. 26 | 27 | If you find a particular ESLint error inapplicable to your code, 28 | you can disable a specific rule for the next line of code with a comment like: 29 | 30 | \if{html}{\out{
}}\preformatted{// eslint-disable-next-line no-restricted-syntax 31 | }\if{html}{\out{
}} 32 | 33 | See the \href{https://eslint.org/docs/user-guide/configuring/rules#using-configuration-comments-1}{ESLint documentation} 34 | for full details. 35 | } 36 | \examples{ 37 | if (interactive()) { 38 | # Lint the JavaScript sources in the `app/js` directory. 39 | lint_js() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /man/lint_r.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{lint_r} 4 | \alias{lint_r} 5 | \title{Lint R} 6 | \usage{ 7 | lint_r(paths = NULL) 8 | } 9 | \arguments{ 10 | \item{paths}{Character vector of directories and files to lint. 11 | When \code{NULL} (the default), check \code{app} and \code{tests/testthat} directories.} 12 | } 13 | \value{ 14 | None. This function is called for side effects. 15 | } 16 | \description{ 17 | Uses the \code{{lintr}} package to check all R sources in the \code{app} and \code{tests/testthat} directories 18 | for style errors. 19 | } 20 | \details{ 21 | The linter rules can be \href{https://lintr.r-lib.org/articles/lintr.html#configuring-linters}{adjusted} 22 | in the \code{.lintr} file. 23 | 24 | You can set the maximum number of accepted style errors 25 | with the \code{legacy_max_lint_r_errors} option in \code{rhino.yml}. 26 | This can be useful when inheriting legacy code with multiple styling issues. 27 | 28 | The \code{\link[box.linters:namespaced_function_calls]{box.linters::namespaced_function_calls()}} linter requires the \code{{treesitter}} and 29 | \code{{treesitter.r}} packages. These require R >= 4.3.0. \code{lint_r()} will continue to run and skip 30 | \code{namespaced_function_calls()} if its dependencies are not available. 31 | } 32 | -------------------------------------------------------------------------------- /man/lint_sass.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{lint_sass} 4 | \alias{lint_sass} 5 | \title{Lint Sass} 6 | \usage{ 7 | lint_sass(fix = FALSE) 8 | } 9 | \arguments{ 10 | \item{fix}{Automatically fix problems.} 11 | } 12 | \value{ 13 | None. This function is called for side effects. 14 | } 15 | \description{ 16 | Runs \href{https://stylelint.io/}{Stylelint} on the Sass sources in the \code{app/styles} directory. 17 | Requires Node.js to be available on the system. 18 | } 19 | \examples{ 20 | if (interactive()) { 21 | # Lint the Sass sources in the `app/styles` directory. 22 | lint_sass() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /man/log.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/log.R 3 | \docType{data} 4 | \name{log} 5 | \alias{log} 6 | \title{Logging functions} 7 | \format{ 8 | An object of class \code{list} of length 7. 9 | } 10 | \usage{ 11 | log 12 | } 13 | \description{ 14 | Convenient way to log messages at a desired severity level. 15 | } 16 | \details{ 17 | The \code{log} object is a list of logging functions, in order of decreasing severity: 18 | \enumerate{ 19 | \item \code{fatal} 20 | \item \code{error} 21 | \item \code{warn} 22 | \item \code{success} 23 | \item \code{info} 24 | \item \code{debug} 25 | \item \code{trace} 26 | } 27 | 28 | Rhino configures logging based on settings read from the \code{config.yml} file 29 | in the root of your project: 30 | \enumerate{ 31 | \item \code{rhino_log_level}: The minimum severity of messages to be logged. 32 | \item \code{rhino_log_file}: The file to save logs to. If \code{NA}, standard error stream will be used. 33 | } 34 | 35 | The default \code{config.yml} file uses \verb{!expr Sys.getenv()} 36 | so that log level and file can also be configured 37 | by setting the \code{RHINO_LOG_LEVEL} and \code{RHINO_LOG_FILE} environment variables. 38 | 39 | The functions re-exported by the \code{log} object are aliases for \code{{logger}} functions. 40 | You can also import the package and use it directly to utilize its full capabilities. 41 | } 42 | \examples{ 43 | \dontrun{ 44 | box::use(rhino[log]) 45 | 46 | # Messages can be formatted using glue syntax. 47 | name <- "Rhino" 48 | log$warn("Hello {name}!") 49 | log$info("{1:3} + {1:3} = {2 * (1:3)}") 50 | } 51 | } 52 | \keyword{datasets} 53 | -------------------------------------------------------------------------------- /man/react_component.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/react.R 3 | \name{react_component} 4 | \alias{react_component} 5 | \title{React components} 6 | \usage{ 7 | react_component(name) 8 | } 9 | \arguments{ 10 | \item{name}{The name of the component.} 11 | } 12 | \value{ 13 | A function representing the component. 14 | } 15 | \description{ 16 | Declare the React components defined in your app. 17 | } 18 | \details{ 19 | There are three steps to add a React component to your Rhino application: 20 | \enumerate{ 21 | \item Define the component using JSX and register it with \code{Rhino.registerReactComponents()}. 22 | \item Declare the component in R with \code{rhino::react_component()}. 23 | \item Use the component in your application. 24 | } 25 | 26 | Please refer to the \href{https://appsilon.github.io/rhino/articles/tutorial/use-react-in-rhino.html}{Tutorial: Use React in Rhino} 27 | to learn about the details. 28 | } 29 | \examples{ 30 | # Declare the component. 31 | TextBox <- react_component("TextBox") 32 | 33 | # Use the component. 34 | ui <- TextBox("Hello!", font_size = 20) 35 | } 36 | -------------------------------------------------------------------------------- /man/rhinos.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/data.R 3 | \docType{data} 4 | \name{rhinos} 5 | \alias{rhinos} 6 | \title{Population of rhinos} 7 | \format{ 8 | A data frame with 58 rows and 3 variables: 9 | \describe{ 10 | \item{Year}{year} 11 | \item{Population}{rhinos population} 12 | \item{Species}{rhinos species} 13 | } 14 | } 15 | \source{ 16 | \url{https://ourworldindata.org/} 17 | } 18 | \usage{ 19 | rhinos 20 | } 21 | \description{ 22 | A dataset containing population of 5 species of rhinos. 23 | } 24 | \keyword{datasets} 25 | -------------------------------------------------------------------------------- /man/test_e2e.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{test_e2e} 4 | \alias{test_e2e} 5 | \title{Run Cypress end-to-end tests} 6 | \usage{ 7 | test_e2e(interactive = FALSE) 8 | } 9 | \arguments{ 10 | \item{interactive}{Should Cypress be run in the interactive mode?} 11 | } 12 | \value{ 13 | None. This function is called for side effects. 14 | } 15 | \description{ 16 | Uses \href{https://www.cypress.io/}{Cypress} to run end-to-end tests 17 | defined in the \code{tests/cypress} directory. 18 | Requires Node.js to be available on the system. 19 | } 20 | \details{ 21 | Check out: 22 | \href{https://appsilon.github.io/rhino/articles/tutorial/write-end-to-end-tests-with-cypress.html}{Tutorial: Write end-to-end tests with Cypress} 23 | to learn how to write end-to-end tests for your Rhino app. 24 | 25 | If you want to write end-to-end tests with \code{{shinytest2}}, see our 26 | \href{https://appsilon.github.io/rhino/articles/how-to/use-shinytest2.html}{How-to: Use shinytest2} 27 | guide. 28 | } 29 | \examples{ 30 | if (interactive()) { 31 | # Run the end-to-end tests in the `tests/cypress` directory. 32 | test_e2e() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /man/test_r.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tools.R 3 | \name{test_r} 4 | \alias{test_r} 5 | \title{Run R unit tests} 6 | \usage{ 7 | test_r(...) 8 | } 9 | \arguments{ 10 | \item{...}{Additional arguments passed to \code{testthat::test_dir()}.} 11 | } 12 | \value{ 13 | None. This function is called for side effects. 14 | } 15 | \description{ 16 | Uses the \code{{testhat}} package to run all unit tests in \code{tests/testthat} directory. 17 | } 18 | \examples{ 19 | if (interactive()) { 20 | # Run all unit tests in the `tests/testthat` directory. 21 | test_r() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkgdown/build.R: -------------------------------------------------------------------------------- 1 | #' @param repo The path to the git repository to build. 2 | #' @param versions A list of lists. Each sublist should contain the following keys: 3 | #' - `git_ref`: The git ref to build. 4 | #' - `url`: The URL path for the version. 5 | #' - `label`: The label to display in the navbar. To use the version from DESCRIPTION provide `TRUE`. 6 | #' Additonally, exactly one version should have `url` set to "/". 7 | #' @param root_url The root URL for all versions of the website. 8 | #' @param destination The destination directory for the built website. 9 | build_versioned <- function(repo, versions, root_url, destination) { 10 | validate_versions(versions) 11 | 12 | # Prepare a repo for building 13 | temp_repo <- fs::dir_copy(repo, fs::file_temp("versioned-build-repo-")) 14 | on.exit(fs::dir_delete(temp_repo)) 15 | # NOTE: detach to avoid git worktree complaining about the current ref being checked out 16 | system2("git", c("-C", temp_repo, "switch", "--detach", "@")) 17 | build_version <- build_version_factory(temp_repo, versions, root_url, destination) 18 | 19 | # NOTE: building the root URL first, so pkgdown doesn't complain about a non-empty destination directory 20 | root_index <- purrr::detect_index(versions, \(x) isTRUE(x$url == "/")) 21 | purrr::walk(c(versions[root_index], versions[-root_index]), build_version) 22 | } 23 | 24 | validate_versions <- function(versions) { 25 | expected_names <- c("git_ref", "url", "label") 26 | n_root <- 0 27 | purrr::walk(versions, function(version) { 28 | diff <- setdiff(expected_names, names(version)) 29 | if (length(diff) > 0) { 30 | stop("A version is missing the following keys: ", paste(diff, collapse = ", ")) 31 | } 32 | if (isTRUE(version$url == "/")) { 33 | n_root <<- n_root + 1 34 | } 35 | }) 36 | if (n_root != 1) { 37 | stop("Exactly one version should have url set to '/'") 38 | } 39 | } 40 | 41 | build_version_factory <- function(repo, versions, root_url, destination) { 42 | version_switcher <- version_switcher_factory(versions, root_url) 43 | destination <- fs::path_abs(destination) 44 | extra_css_path <- fs::path_join(c(repo, "pkgdown", "extra.css")) 45 | 46 | function(version) { 47 | # Prepare a worktree for building 48 | build_dir <- fs::file_temp("versioned-build-worktree-") 49 | on.exit(system2("git", c("-C", repo, "worktree", "remove", "--force", build_dir))) # NOTE: --force because we overwrite extra.css 50 | status <- system2("git", c("-C", repo, "worktree", "add", build_dir, version$git_ref)) 51 | if (status != 0) { 52 | stop("Failed to create a worktree for ref ", version$git_ref) 53 | } 54 | 55 | # Write extra.css 56 | fs::file_copy(extra_css_path, fs::path_join(c(build_dir, "pkgdown", "extra.css")), overwrite = TRUE) 57 | 58 | # NOTE: providing an absolute path to build_site won't work: https://github.com/r-lib/pkgdown/issues/2172 59 | withr::with_dir(build_dir, { 60 | config <- yaml::read_yaml("pkgdown/_pkgdown.yml") 61 | pkgdown::build_site_github_pages( 62 | override = list( 63 | url = sub("/$", "", url_join(root_url, version$url)), 64 | navbar = list(type = "light"), 65 | template = list( 66 | includes = list( 67 | # Prepend the version switcher to before_navbar instead of overwriting it. 68 | before_navbar = paste( 69 | version_switcher(version), 70 | config$template$includes$before_navbar, 71 | sep = "\n" 72 | ) 73 | ) 74 | ) 75 | ), 76 | dest_dir = fs::path_join(c(destination, version$url)) 77 | ) 78 | }) 79 | } 80 | } 81 | 82 | url_join <- function(url, path) { 83 | paste( 84 | sub("/$", "", url), 85 | sub("^/", "", path), 86 | sep = "/" 87 | ) 88 | } 89 | 90 | version_switcher_factory <- function(versions, root_url) { 91 | wrap_label <- function(label) { 92 | if (isTRUE(label)) { 93 | label <- paste(desc::desc_get_version(), "(dev)") 94 | } 95 | label 96 | } 97 | version_list <- purrr::map( 98 | versions, 99 | function(ver) { 100 | htmltools::tags$li( 101 | htmltools::a( 102 | class = "dropdown-item", 103 | href = url_join(root_url, ver$url), 104 | wrap_label(ver$label) 105 | ) 106 | ) 107 | } 108 | ) 109 | function(version) { 110 | htmltools::div( 111 | id = "version-switcher", 112 | class = "dropdown", 113 | htmltools::a( 114 | href = "#", 115 | class = "nav-link dropdown-toggle", 116 | role = "button", 117 | `data-bs-toggle` = "dropdown", 118 | `aria-expanded` = "false", 119 | `aria-haspopup` = "true", 120 | wrap_label(version$label) 121 | ), 122 | htmltools::tags$ul( 123 | class = "dropdown-menu", 124 | version_list 125 | ) 126 | ) |> as.character() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /pkgdown/extra.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #7b7cd2; 3 | } 4 | 5 | .navbar { 6 | --bs-primary-rgb: 123, 124, 210; 7 | } 8 | 9 | .navbar a.dropdown-item:hover { 10 | background-color: var(--primary-color); 11 | } 12 | 13 | .navbar .navbar-nav .nav-item.active > .nav-link { 14 | color: #fff; 15 | background-color: var(--primary-color); 16 | } 17 | 18 | .navbar .navbar-nav .nav-item > .nav-link { 19 | color: rgba(255, 255, 255, 0.8); 20 | 21 | &:hover { 22 | background-color: var(--primary-color); 23 | color: #fff; 24 | } 25 | } 26 | 27 | .navbar-brand { 28 | --bs-navbar-brand-color: #fff; 29 | 30 | &:hover { 31 | --bs-navbar-brand-hover-color: rgba(255, 255, 255, 0.8); 32 | } 33 | } 34 | 35 | a { 36 | color: var(--primary-color); 37 | } 38 | 39 | a.nav-link, 40 | .home { 41 | color: hsl(0, 0%, 80%); 42 | 43 | &:hover { 44 | color: hsl(0, 0%, 100%); 45 | } 46 | } 47 | 48 | a:hover { 49 | color: #2c2b2b; 50 | } 51 | 52 | button.btn.btn-primary.btn-copy-ex { 53 | background-color: var(--primary-color); 54 | border-color: var(--primary-color); 55 | } 56 | 57 | .home { 58 | display: none; 59 | left: 0px; 60 | position: absolute; 61 | padding: 8px 30px; 62 | 63 | @media (min-width: 1250px) { 64 | display: initial; 65 | } 66 | } 67 | 68 | /* Hide the version number - it's in the version switcher injected by pkgdown/build.R. */ 69 | .navbar .nav-text { 70 | display: none; 71 | } 72 | 73 | #version-switcher { 74 | margin-inline-start: 0.5rem; 75 | margin-inline-end: auto; 76 | 77 | @media (min-width: 992px) { 78 | margin-inline-end: 2rem; 79 | } 80 | 81 | & > a { 82 | background-color: hsl(239.3, 40.2%, 55.3%); 83 | color: rgba(255, 255, 255, 0.8); 84 | padding: 0.25rem 1rem; 85 | border-radius: 1rem; 86 | 87 | &:hover { 88 | color: white; 89 | } 90 | } 91 | } 92 | 93 | summary:has(h3#past) { 94 | & > * { 95 | display: inline; 96 | } 97 | 98 | &::marker, 99 | &::-webkit-details-marker { 100 | /* an equivalent of Bootstrap's h3 that's within the summary */ 101 | font-size: calc(1.3rem + 0.6vw); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /pkgdown/versions.yml: -------------------------------------------------------------------------------- 1 | - git_ref: 'refs/remotes/origin/main' 2 | url: /dev 3 | label: true 4 | - git_ref: 'refs/tags/v1.11.0' 5 | url: '/' 6 | label: '1.11' 7 | - git_ref: 'refs/tags/v1.10.1' 8 | url: '/v1.10.1' 9 | label: '1.10' 10 | - git_ref: 'refs/tags/v1.9.0' 11 | url: '/v1.9.0' 12 | label: '1.9' 13 | - git_ref: 'refs/tags/v1.8.0' 14 | url: '/v1.8.0' 15 | label: '1.8' 16 | - git_ref: 'refs/tags/v1.7.0' 17 | url: '/v1.7.0' 18 | label: '1.7' 19 | - git_ref: 'refs/tags/v1.6.0' 20 | url: '/v1.6.0' 21 | label: '1.6' 22 | - git_ref: 'refs/tags/v1.5.0' 23 | url: '/v1.5.0' 24 | label: '1.5' 25 | - git_ref: 'refs/tags/v1.4.0' 26 | url: '/v1.4.0' 27 | label: '1.4' 28 | - git_ref: 'refs/tags/v1.3.1' 29 | url: '/v1.3.1' 30 | label: '1.3' 31 | - git_ref: 'refs/tags/v1.2.1' 32 | url: '/v1.2.1' 33 | label: '1.2' 34 | - git_ref: 'refs/tags/v1.1.1' 35 | url: '/v1.1.1' 36 | label: '1.1' 37 | - git_ref: 'refs/tags/v1.0.0' 38 | url: '/v1.0.0' 39 | label: '1.0' 40 | -------------------------------------------------------------------------------- /rhino.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | ProjectId: d115d13b-053e-44b1-ae8b-af45c16fec84 3 | 4 | RestoreWorkspace: Default 5 | SaveWorkspace: Default 6 | AlwaysSaveHistory: Default 7 | 8 | EnableCodeIndexing: Yes 9 | UseSpacesForTab: Yes 10 | NumSpacesForTab: 2 11 | Encoding: UTF-8 12 | 13 | RnwWeave: Sweave 14 | LaTeX: pdfLaTeX 15 | 16 | BuildType: Package 17 | PackageUseDevtools: Yes 18 | PackageInstallArgs: --no-multiarch --with-keep.source 19 | -------------------------------------------------------------------------------- /tests/e2e/app-files/Box.jsx: -------------------------------------------------------------------------------- 1 | const { useState } = React; 2 | 3 | export default function Box({ id, children }) { 4 | const [visible, setVisible] = useState(false); 5 | return ( 6 |
7 | 10 | {visible && children} 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /tests/e2e/app-files/config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | rhino_log_level: TRACE 3 | rhino_log_file: log.txt 4 | -------------------------------------------------------------------------------- /tests/e2e/app-files/hello.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny, 3 | ) 4 | 5 | box::use( 6 | app/logic/say_hello[say_hello], 7 | ) 8 | 9 | #' @export 10 | ui <- function(id) { 11 | ns <- shiny$NS(id) 12 | shiny$bootstrapPage( 13 | shiny$tags$div( 14 | class = "input-and-click", 15 | shiny$textInput(ns("name"), label = NULL, value = NULL), 16 | shiny$actionButton(ns("say_hello"), label = "Say Hello") 17 | ), 18 | shiny$textOutput(ns("message")) 19 | ) 20 | } 21 | 22 | #' @export 23 | server <- function(id) { 24 | shiny$moduleServer(id, function(input, output, session) { 25 | ns <- session$ns 26 | 27 | shiny$observe({ 28 | is_name_empty <- is.null(input$name) || input$name == "" 29 | 30 | session$sendCustomMessage( 31 | "toggleDisable", 32 | list(id = paste0("#", ns("say_hello")), disable = is_name_empty) 33 | ) 34 | }) 35 | 36 | shiny$observeEvent( 37 | input$say_hello, { 38 | output$message <- shiny$renderText(say_hello(shiny$isolate(input$name))) 39 | } 40 | ) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /tests/e2e/app-files/hello.cy.js: -------------------------------------------------------------------------------- 1 | describe('Say Hello', () => { 2 | beforeEach(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | it('should save trace log in log.txt', () => { 7 | const filePath = '../log.txt'; 8 | 9 | cy.readFile(filePath) 10 | .then(fileContents => { 11 | expect(fileContents).to.match(/^TRACE.+This is a test/); 12 | }); 13 | }); 14 | 15 | it('should have an empty input, disabled button, and no message on start up', () => { 16 | cy.get('#app-hello-say_hello').should('be.disabled'); 17 | cy.get('#app-hello-name').should('have.value', ''); 18 | cy.get('#app-hello-message').should('not.have.text'); 19 | }); 20 | 21 | it('should enable button on input and display message on click', () => { 22 | const inputName = 'Rhino'; 23 | cy.get('#app-hello-name').type(inputName); 24 | 25 | cy.get('#app-hello-say_hello').should('not.be.disabled'); 26 | cy.get('#app-hello-say_hello').click(); 27 | 28 | cy.get('#app-hello-message').should('have.text', `Hello, ${inputName}!`); 29 | }); 30 | 31 | it('should disable button when text is cleared', () => { 32 | const inputName = 'Rhino'; 33 | cy.get('#app-hello-name').type(inputName); 34 | 35 | cy.get('#app-hello-say_hello').should('not.be.disabled'); 36 | cy.get('#app-hello-say_hello').click(); 37 | 38 | cy.get('#app-hello-name').clear(); 39 | cy.get('#app-hello-say_hello').should('be.disabled'); 40 | }); 41 | 42 | it('should style elements', () => { 43 | const inputName = 'Rhino'; 44 | cy.get('#app-hello-name').type(inputName); 45 | cy.get('#app-hello-say_hello').click(); 46 | 47 | cy.get('.input-and-click') 48 | .should('have.css', 'display', 'inline-flex'); 49 | 50 | cy.get('#app-hello-say_hello') 51 | .should('have.css', 'color', 'rgb(255, 255, 255)') 52 | // check if border: none 53 | .and('have.css', 'border-width', '0px') 54 | .and('have.css', 'border-style', 'none') 55 | .and('have.css', 'border-color', 'rgb(255, 255, 255)') 56 | .and('have.css', 'background-color', 'rgb(0, 153, 249)'); 57 | 58 | cy.get('#app-hello-message') 59 | .should('have.css', 'display', 'flex') 60 | .and('have.css', 'align-items', 'center') 61 | .and('have.css', 'justify-content', 'center'); 62 | }); 63 | 64 | it('should work with React components', () => { 65 | cy.get('#app-box button').click(); 66 | cy.get('#app-box p').contains('React works!'); 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /tests/e2e/app-files/index.js: -------------------------------------------------------------------------------- 1 | import Box from './Box'; 2 | 3 | Rhino.registerReactComponents({ Box }); 4 | 5 | Shiny.addCustomMessageHandler('toggleDisable', (message) => { 6 | $(message.id).attr('disabled', message.disable); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/e2e/app-files/main.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | rhino[log, react_component], 3 | shiny, 4 | ) 5 | 6 | box::use(app/view/hello, ) 7 | 8 | Box <- react_component("Box") # nolint object_name_linter 9 | 10 | #' @export 11 | ui <- function(id) { 12 | ns <- shiny$NS(id) 13 | shiny$tagList( 14 | Box(id = ns("box"), shiny$p("React works!")), 15 | hello$ui(ns("hello")) 16 | ) 17 | } 18 | 19 | #' @export 20 | server <- function(id) { 21 | shiny$moduleServer(id, function(input, output, session) { 22 | log$trace("This is a test") 23 | hello$server("hello") 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /tests/e2e/app-files/main.scss: -------------------------------------------------------------------------------- 1 | .input-and-click { 2 | display: inline-flex; 3 | } 4 | 5 | body > div.input-and-click > div { 6 | margin: 1rem; 7 | margin-right: 0.5rem; 8 | } 9 | 10 | #app-hello-say_hello { 11 | margin: 1rem; 12 | margin-left: 0.5rem; 13 | color: white; 14 | border: none; 15 | background-color: #0099f9; 16 | } 17 | 18 | #app-hello-message { 19 | font-size: 10rem; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | height: 40rem; 24 | } 25 | -------------------------------------------------------------------------------- /tests/e2e/app-files/say_hello.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | say_hello <- function(name) { 3 | paste0("Hello, ", name, "!") 4 | } 5 | -------------------------------------------------------------------------------- /tests/e2e/app-files/test-hello.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[testServer], 3 | testthat[describe, expect_identical, it], 4 | ) 5 | box::use( 6 | app/view/hello[server], 7 | ) 8 | 9 | describe("hello$server()", { 10 | it("should print the correct message", { 11 | testServer(server, { 12 | session$setInputs(name = "Rhino", say_hello = 1) 13 | expect_identical(output$message, "Hello, Rhino!") 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /tests/e2e/app-files/test-say_hello.R: -------------------------------------------------------------------------------- 1 | box::use(testthat[describe, expect_identical, it], ) 2 | 3 | box::use(app/logic/say_hello[say_hello], ) 4 | 5 | describe("say_hello()", { 6 | it("should say hello with the correct name", { 7 | expect_identical(say_hello("Rhino"), "Hello, Rhino!") 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/e2e/test-box-lsp.R: -------------------------------------------------------------------------------- 1 | testthat::expect_false( 2 | is.null(getOption("languageserver.parser_hooks")) 3 | ) 4 | -------------------------------------------------------------------------------- /tests/e2e/test-build-js.R: -------------------------------------------------------------------------------- 1 | min_js_path <- fs::path("app", "static", "js", "app.min.js") 2 | index_js_path <- fs::path("app", "js", "index.js") 3 | 4 | # Create minimal css and check app.min.css output 5 | cat( 6 | "function sayHello() { console.log('Hello'); } export { sayHello };\n", 7 | file = index_js_path 8 | ) 9 | rhino::build_js() 10 | # Checks if the built js file is not a result of an empty index.js 11 | # The main test to see if build_js() work should be in the Cypress test 12 | testthat::expect_true(readLines(min_js_path) != "var App;App={};") 13 | 14 | # Revert to empty script and check otuput 15 | cat( 16 | "\n", 17 | file = index_js_path 18 | ) 19 | rhino::build_js() 20 | testthat::expect_identical( 21 | readLines(min_js_path, warn = FALSE), 22 | "var App;App={};" 23 | ) 24 | -------------------------------------------------------------------------------- /tests/e2e/test-build-sass.R: -------------------------------------------------------------------------------- 1 | min_css_path <- fs::path("app", "static", "css", "app.min.css") 2 | main_css_path <- fs::path("app", "styles", "main.scss") 3 | 4 | # Create minimal css and check app.min.css output 5 | cat( 6 | ".myClass { color: #fff; }\n", 7 | file = main_css_path 8 | ) 9 | rhino::build_sass() 10 | testthat::expect_identical( 11 | readLines(min_css_path), 12 | ".myClass{color:#fff}" 13 | ) 14 | 15 | # Revert to empty CSS and check output 16 | cat("\n", file = main_css_path 17 | ) 18 | rhino::build_sass() 19 | testthat::expect_identical( 20 | readLines(min_css_path), "" 21 | ) 22 | -------------------------------------------------------------------------------- /tests/e2e/test-custom-npm.R: -------------------------------------------------------------------------------- 1 | local({ 2 | tmp <- withr::local_tempdir() 3 | wrapper_path <- fs::path(tmp, "wrapper") 4 | touch_path <- fs::path(tmp, "it_works") 5 | 6 | # Prepare a wrapper script which creates an "it_works" file and runs npm. 7 | fs::file_create(wrapper_path, mode = "u=rwx") 8 | writeLines( 9 | c( 10 | "#!/bin/sh", 11 | paste("touch", touch_path), 12 | 'exec npm "$@"' 13 | ), 14 | wrapper_path 15 | ) 16 | 17 | # Use the wrapper script instead of npm. 18 | withr::local_envvar(RHINO_NPM = wrapper_path) 19 | rhino:::npm("--version") 20 | 21 | testthat::expect_true(fs::file_exists(touch_path)) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/e2e/test-dependencies.R: -------------------------------------------------------------------------------- 1 | # Check if package is installed without loading or attaching it. 2 | is_installed <- function(package) { 3 | length(find.package(package, quiet = TRUE)) > 0 4 | } 5 | 6 | initial_dependencies <- readLines("dependencies.R") 7 | initial_lockfile <- readLines("renv.lock") 8 | 9 | # Check initial state. 10 | testthat::expect_false(is_installed("dplyr")) 11 | testthat::expect_false(any(initial_dependencies == "library(dplyr)")) 12 | testthat::expect_false(any(initial_lockfile == ' "dplyr": {')) 13 | 14 | # Install package and check if it was done correctly. 15 | rhino::pkg_install("dplyr") 16 | testthat::expect_true(is_installed("dplyr")) 17 | testthat::expect_setequal( 18 | readLines("dependencies.R"), 19 | c(initial_dependencies, "library(dplyr)") 20 | ) 21 | testthat::expect_contains( 22 | readLines("renv.lock"), 23 | ' "dplyr": {' 24 | ) 25 | 26 | # Remove package and check if we're back to initial state. 27 | rhino::pkg_remove("dplyr") 28 | testthat::expect_false(is_installed("dplyr")) 29 | testthat::expect_identical(readLines("dependencies.R"), initial_dependencies) 30 | testthat::expect_identical(readLines("renv.lock"), initial_lockfile) 31 | 32 | # install package from GitHub 33 | 34 | initial_dependencies <- readLines("dependencies.R") 35 | initial_lockfile <- readLines("renv.lock") 36 | 37 | # Check initial state. 38 | testthat::expect_false(is_installed("shiny.i18n")) 39 | testthat::expect_false(any(initial_dependencies == "library(shiny.i18n)")) 40 | testthat::expect_false(any(initial_lockfile == ' "shiny.i18n": {')) 41 | 42 | # Install package and check if it was done correctly. 43 | rhino::pkg_install("Appsilon/shiny.i18n") 44 | testthat::expect_true(is_installed("shiny.i18n")) 45 | testthat::expect_setequal( 46 | readLines("dependencies.R"), 47 | c(initial_dependencies, "library(shiny.i18n)") 48 | ) 49 | testthat::expect_contains( 50 | readLines("renv.lock"), 51 | ' "shiny.i18n": {' 52 | ) 53 | 54 | # Remove package and check if we're back to initial state. 55 | rhino::pkg_remove("shiny.i18n") 56 | testthat::expect_false(is_installed("shiny.i18n")) 57 | testthat::expect_identical(readLines("dependencies.R"), initial_dependencies) 58 | testthat::expect_identical(readLines("renv.lock"), initial_lockfile) 59 | 60 | 61 | # install package from Bioconductor 62 | 63 | initial_dependencies <- readLines("dependencies.R") 64 | initial_lockfile <- readLines("renv.lock") 65 | 66 | # Check initial state. 67 | testthat::expect_false(is_installed("Biobase")) 68 | testthat::expect_false(any(initial_dependencies == "library(Biobase)")) 69 | testthat::expect_false(any(initial_lockfile == ' "Biobase": {')) 70 | 71 | # Install package and check if it was done correctly. 72 | rhino::pkg_install("bioc::Biobase") 73 | testthat::expect_true(is_installed("Biobase")) 74 | testthat::expect_setequal( 75 | readLines("dependencies.R"), 76 | c(initial_dependencies, "library(Biobase)") 77 | ) 78 | testthat::expect_contains( 79 | readLines("renv.lock"), 80 | ' "Biobase": {' 81 | ) 82 | 83 | # Remove package and check if we're back to initial state. 84 | rhino::pkg_remove("Biobase") 85 | testthat::expect_false(is_installed("Biobase")) 86 | testthat::expect_identical(readLines("dependencies.R"), initial_dependencies) 87 | testthat::expect_identical(readLines("renv.lock"), initial_lockfile) 88 | -------------------------------------------------------------------------------- /tests/e2e/test-format-js.R: -------------------------------------------------------------------------------- 1 | rhino::format_js() 2 | 3 | # Create bad scripts and test if formatting returns the expected result 4 | test_file_path <- fs::path("app", "js", "bad-style.js") 5 | cat('const someFunction = (a ,b) => a+ b + "asdf"', file = test_file_path) 6 | rhino::format_js() 7 | testthat::expect_identical( 8 | readLines(test_file_path), 9 | "const someFunction = (a, b) => a + b + 'asdf';" 10 | ) 11 | 12 | # Clean up 13 | file.remove(test_file_path) 14 | -------------------------------------------------------------------------------- /tests/e2e/test-format-r.R: -------------------------------------------------------------------------------- 1 | rhino::format_r(paths = c("app", "tests")) 2 | 3 | # Create bad scripts and test if formatting returns the expected result 4 | test_file_path <- fs::path("app", "logic", "bad-style.R") 5 | cat("bad_object_style=12", file = test_file_path) 6 | rhino::format_r(paths = "app") 7 | testthat::expect_identical( 8 | readLines(test_file_path), 9 | "bad_object_style <- 12" 10 | ) 11 | 12 | # Clean up 13 | file.remove(test_file_path) 14 | -------------------------------------------------------------------------------- /tests/e2e/test-format-sass.R: -------------------------------------------------------------------------------- 1 | rhino::format_sass() 2 | 3 | # Create bad scripts and test if formatting returns the expected result 4 | test_file_path <- fs::path("app", "styles", "bad-style.scss") 5 | cat("@import 'asdf';\nx+y{ color: red}", file = test_file_path) 6 | rhino::format_sass() 7 | testthat::expect_identical( 8 | readLines(test_file_path), 9 | c( 10 | '@import "asdf";', 11 | "x + y {", 12 | " color: red;", 13 | "}" 14 | ) 15 | ) 16 | 17 | # Clean up 18 | file.remove(test_file_path) 19 | -------------------------------------------------------------------------------- /tests/e2e/test-lint-js.R: -------------------------------------------------------------------------------- 1 | rhino::lint_js() 2 | 3 | # Create bad scripts and test if formatting returns the expected result 4 | test_js_path <- fs::path("app", "js", "badStyle.js") 5 | cat("function sayHello() {console.log('Hello')}; export{sayHello};", file = test_js_path) 6 | testthat::expect_error(rhino::lint_js()) 7 | rhino::lint_js(fix = TRUE) 8 | testthat::expect_identical( 9 | readLines(test_js_path), 10 | "function sayHello() { console.log('Hello'); } export { sayHello };" 11 | ) 12 | # Clean up 13 | file.remove(test_js_path) 14 | -------------------------------------------------------------------------------- /tests/e2e/test-lint-r.R: -------------------------------------------------------------------------------- 1 | install.packages(c("treesitter", "treesitter.r")) 2 | 3 | rhino::lint_r() 4 | # Create bad scripts and test if formatting returns the expected result 5 | test_file_path <- fs::path("app", "logic", "bad-style.R") 6 | cat("bad_object_style=12", file = test_file_path) 7 | testthat::expect_error(rhino::lint_r()) 8 | # Clean up 9 | file.remove(test_file_path) 10 | -------------------------------------------------------------------------------- /tests/e2e/test-lint-sass.R: -------------------------------------------------------------------------------- 1 | rhino::lint_sass() 2 | 3 | # Create bad scripts and test if formatting returns the expected result 4 | test_scss_path <- fs::path("app", "styles", "bad-style.scss") 5 | cat(".my-class{color: #FFFFFF}", file = test_scss_path) 6 | testthat::expect_error(rhino::lint_sass()) 7 | rhino::lint_sass(fix = TRUE) 8 | testthat::expect_identical( 9 | readLines(test_scss_path), 10 | ".my-class { color: #fff; }" 11 | ) 12 | # Clean up 13 | file.remove(test_scss_path) 14 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(rhino) 3 | 4 | test_check("rhino") 5 | -------------------------------------------------------------------------------- /tests/testthat/helpers/main.scss: -------------------------------------------------------------------------------- 1 | .components-container { 2 | display: inline-grid; 3 | grid-template-columns: 1fr 1fr; 4 | width: 100%; 5 | 6 | .component-box { 7 | padding: 10px; 8 | margin: 10px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/testthat/test-app.R: -------------------------------------------------------------------------------- 1 | describe("configure_logger()", { 2 | it("works with missing config fields", { 3 | mockery::stub(configure_logger, "config::get", list()) 4 | expect_message(configure_logger()) 5 | 6 | mockery::stub(configure_logger, "config::get", list(rhino_log_level = "INFO")) 7 | expect_message(configure_logger()) 8 | 9 | mockery::stub(configure_logger, "config::get", list(rhino_log_file = "my.log")) 10 | expect_message(configure_logger()) 11 | 12 | mockery::stub( 13 | configure_logger, 14 | "config::get", 15 | list(rhino_log_level = "INFO", rhino_log_file = "my.log") 16 | ) 17 | expect_silent(configure_logger()) 18 | }) 19 | }) 20 | 21 | describe("normalize_main()", { 22 | it("handles a Shiny module", { 23 | main <- list( 24 | ui = function(id) shiny::tags$div("test"), 25 | server = function(id) { 26 | shiny::moduleServer(id, function(input, output, session) {}) 27 | } 28 | ) 29 | wrapped <- normalize_main(main, is_module = TRUE) 30 | expect_identical(names(formals(wrapped$ui)), c("request")) 31 | expect_identical(names(formals(wrapped$server)), c("input", "output", "session")) 32 | }) 33 | }) 34 | 35 | describe("normalize_ui()", { 36 | it("handles UI defined as a Shiny module", { 37 | ui <- function(id) shiny::tags$div("test") 38 | wrapped <- normalize_ui(ui, is_module = TRUE) 39 | expect_identical(wrapped("request"), ui("app")) 40 | }) 41 | 42 | it("handles UI defined as a tag", { 43 | ui <- shiny::tags$div("test") 44 | wrapped <- normalize_ui(ui) 45 | expect_identical(wrapped("request"), ui) 46 | }) 47 | 48 | it("handles UI defined as a function without parameters", { 49 | ui <- function() shiny::tags$div("test") 50 | wrapped <- normalize_ui(ui) 51 | expect_identical(wrapped("request"), ui()) 52 | }) 53 | 54 | it("handles UI defined as a function with a request parameter", { 55 | ui <- function(request) shiny::tags$div(request) 56 | wrapped <- normalize_ui(ui) 57 | expect_identical(wrapped("request"), ui("request")) 58 | }) 59 | }) 60 | 61 | describe("normalize_server()", { 62 | it("handles server defined as a Shiny module", { 63 | server <- function(id) { 64 | shiny::moduleServer(id, function(input, output, session) {}) 65 | } 66 | wrapped <- normalize_server(server, is_module = TRUE) 67 | expect_identical(names(formals(wrapped)), c("input", "output", "session")) 68 | }) 69 | 70 | it("handles server wihout session paramter", { 71 | server <- function(input, output) {} 72 | wrapped <- normalize_server(server) 73 | expect_identical(names(formals(wrapped)), c("input", "output", "session")) 74 | }) 75 | 76 | it("handles server with session parameter", { 77 | server <- function(input, output, session) {} 78 | wrapped <- normalize_server(server) 79 | expect_identical(names(formals(wrapped)), c("input", "output", "session")) 80 | }) 81 | }) 82 | 83 | describe("warn_on_error()", { 84 | it("catches an error and prints it with an appended message", { 85 | expect_message( 86 | warn_on_error(stop("some_error"), "some_message"), 87 | "some_message: some_error" 88 | ) 89 | }) 90 | }) 91 | 92 | describe("with_head_tags()", { 93 | it("attaches a head tag to UI", { 94 | ui <- function(request) shiny::tags$div("test") 95 | wrapped <- with_head_tags(ui) 96 | first_tag <- wrapped("request")[[1]]$name 97 | expect_identical(first_tag, "head") 98 | }) 99 | }) 100 | 101 | describe("fix_server()", { 102 | it("ensures server uses curly braces and has source reference information attached", { 103 | body_uses_curly_braces <- function(f) { 104 | identical(body(f)[[1]], rlang::sym("{")) 105 | } 106 | 107 | server <- eval(parse( 108 | text = "function(input, output, session) 42", 109 | keep.source = FALSE 110 | )) 111 | fixed <- fix_server(server) 112 | 113 | expect_identical(fixed(), server()) 114 | expect_false(body_uses_curly_braces(server)) 115 | expect_true(body_uses_curly_braces(fixed)) 116 | expect_false("srcref" %in% names(attributes(server))) 117 | expect_true("srcref" %in% names(attributes(fixed))) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /tests/testthat/test-config.R: -------------------------------------------------------------------------------- 1 | test_that("validate_config() checks option fields", { 2 | def <- list( 3 | list( 4 | name = "field", 5 | validator = option_validator("apples", "bananas"), 6 | required = FALSE 7 | ) 8 | ) 9 | expect_silent(validate_config(def, list())) 10 | expect_silent(validate_config(def, list(field = "apples"))) 11 | expect_silent(validate_config(def, list(field = "bananas"))) 12 | expect_error(validate_config(def, list(field = "cherries"))) 13 | }) 14 | 15 | test_that("validate_config() checks integer fields", { 16 | def <- list( 17 | list( 18 | name = "field", 19 | validator = positive_integer_validator, 20 | required = FALSE 21 | ) 22 | ) 23 | expect_silent(validate_config(def, list())) 24 | expect_silent(validate_config(def, list(field = 1L))) 25 | expect_error(validate_config(def, list(field = 1.))) 26 | expect_error(validate_config(def, list(field = "1"))) 27 | }) 28 | 29 | test_that("validate_config() checks for required fields", { 30 | def <- list( 31 | list( 32 | name = "field", 33 | options = positive_integer_validator, 34 | required = TRUE 35 | ) 36 | ) 37 | expect_error(validate_config(def, list())) 38 | }) 39 | 40 | test_that("validate_config() rejects unknown fields", { 41 | def <- list() 42 | expect_error(validate_config(def, list(field = "hello"))) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/testthat/test-dependencies.R: -------------------------------------------------------------------------------- 1 | describe("extract_package_name", { 2 | it("returns the package name intact when using only the package name", { 3 | expect_equal(extract_package_name("shiny"), "shiny") 4 | }) 5 | 6 | it("returns the package name intact when using the package name and version", { 7 | expect_equal(extract_package_name("shiny@1.6.0"), "shiny") 8 | }) 9 | 10 | it("returns the package name when installing a package from GitHub", { 11 | expect_equal(extract_package_name("r-lib/httr"), "httr") 12 | expect_equal(extract_package_name("r-lib/testthat@c67018fa4970"), "testthat") 13 | }) 14 | 15 | it("returns the package name when installing a package from a local path", { 16 | expect_equal(extract_package_name("~/path/to/package"), "package") 17 | }) 18 | 19 | it("returns the package name when installing a package from Bioconductor", { 20 | expect_equal(extract_package_name("bioc::Biobase"), "Biobase") 21 | }) 22 | }) 23 | 24 | describe("extract_packages_names", { 25 | it("returns a vector of package names when installing multiple packages", { 26 | expect_equal(extract_packages_names(c("shiny", "dplyr")), c("shiny", "dplyr")) 27 | }) 28 | }) 29 | 30 | describe("pkg_install", { 31 | it("throws an error when the argument is not a character vector", { 32 | expect_error(pkg_install(1)) 33 | }) 34 | }) 35 | 36 | describe("pkg_remove", { 37 | it("throws an error when the argument is not a character vector", { 38 | expect_error(pkg_remove(1)) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/testthat/test-destructure.R: -------------------------------------------------------------------------------- 1 | describe("%<-% - argument validation", { 2 | it("throws an error when the LHS isn't a call", { 3 | expect_error(asdf %<-% list(a = 123, b = 456)) 4 | }) 5 | 6 | it("throws an error when the LHS isn't a c() call", { 7 | expect_error(x() %<-% list(a = 123, b = 456)) 8 | }) 9 | 10 | it("throws an error when the LHS is a c() call, but contains ellipsis", { 11 | expect_error(c(...) %<-% list(a = 123, b = 456)) 12 | expect_error(c(a, ...) %<-% list(a = 123, b = 456)) 13 | }) 14 | 15 | it("throws an error when the RHS is something else than a list", { 16 | expect_error(c(a, b) %<-% asdf()) 17 | expect_error(c(a, b) %<-% function() {}) 18 | expect_error(c(a, b) %<-% c(1, 2)) 19 | expect_error(c(a, b) %<-% new.env()) 20 | }) 21 | 22 | it("throws an error when the RHS is a data.frame", { 23 | expect_error(c(a, b) %<-% data.frame()) 24 | }) 25 | 26 | it("throws an error when the RHS is a list, but an unnamed one", { 27 | expect_error(c(a, b) %<-% list(1, 2)) 28 | }) 29 | }) 30 | 31 | describe("%<-% - destructuring", { 32 | it("destructures a list with 1 element by name", { 33 | # Arrange 34 | rhs <- list(x = 123) 35 | 36 | # Act 37 | c(x) %<-% rhs 38 | 39 | # Assert 40 | expect_equal(x, 123) 41 | }) 42 | 43 | it("assigns values of a list with unsorted names by names", { 44 | # Arrange 45 | rhs <- list(z = 789, x = 123, y = 456) 46 | 47 | # Act 48 | c(x, y, z) %<-% rhs 49 | 50 | # Assert 51 | expect_equal(x, 123) 52 | expect_equal(y, 456) 53 | expect_equal(z, 789) 54 | }) 55 | 56 | it("assigns only to the values present on the the LHS", { 57 | # Arrange 58 | rhs <- list(z = 789, x = 123, y = 456) 59 | 60 | # Act 61 | c(x, z) %<-% rhs 62 | 63 | # Assert 64 | expect_equal(x, 123) 65 | expect_error(y) 66 | expect_equal(z, 789) 67 | }) 68 | 69 | it("throws an error when the LHS has a name not present on the RHS", { 70 | expect_error(c(a) %<-% list(x = 123, y = 456)) 71 | }) 72 | 73 | it("handles the native pipe operator (|>) when the RHS expression is wrapped in brackets", { 74 | # Act 75 | c(y) %<-% ( 76 | 123 |> 77 | list(x = 123, y = _) 78 | ) 79 | 80 | # Assert 81 | expect_equal(y, 123) 82 | }) 83 | 84 | it("works with functions that return a list", { 85 | # Arrange 86 | get_person <- function() { 87 | list( 88 | name = "John Doe", 89 | age = 30, 90 | email = "john@example.com" 91 | ) 92 | } 93 | 94 | # Act 95 | c(name, age) %<-% get_person() 96 | 97 | # Assert 98 | expect_equal(name, "John Doe") 99 | expect_equal(age, 30) 100 | }) 101 | 102 | it("fails when destructuring non-existent names from a function return value", { 103 | # Arrange 104 | get_person <- function() { 105 | list( 106 | name = "John Doe", 107 | age = 30 108 | ) 109 | } 110 | 111 | # Assert 112 | expect_error(c(name, phone) %<-% get_person()) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /tests/testthat/test-rhino.R: -------------------------------------------------------------------------------- 1 | test_that("rename_template_path() works", { 2 | path <- fs::path("dot.hidden", "app.Rproj.template") 3 | expected <- fs::path(".hidden", "app.Rproj") 4 | expect_identical(rename_template_path(path), expected) 5 | }) 6 | 7 | test_that("rename_template_path() is not too eager", { 8 | path1 <- fs::path("dots") 9 | path2 <- fs::path("atemplate") 10 | expect_identical(rename_template_path(path1), path1) 11 | expect_identical(rename_template_path(path2), path2) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/testthat/test-tools.R: -------------------------------------------------------------------------------- 1 | test_that("starts_with is able to detect paths that start with a given prefix", { 2 | expect_true(starts_with("app/logic/utils.R", "app/logic")) 3 | expect_true(starts_with("app/main.R", "app")) 4 | expect_false(starts_with("app/view/module.R", "app/logic")) 5 | }) 6 | 7 | test_that("build_sass_r builds a minified CSS file out of a Sass file", { 8 | wd <- getwd() 9 | 10 | withr::with_tempdir({ 11 | fs::dir_create("app", "styles") 12 | fs::file_copy( 13 | fs::path(wd, "helpers", "main.scss"), 14 | fs::path("app", "styles", "main.scss") 15 | ) 16 | 17 | build_sass_r() 18 | 19 | minified_css_path <- fs::path("app", "static", "css", "app.min.css") 20 | 21 | expect_true(fs::file_exists(minified_css_path)) 22 | 23 | output <- readLines(minified_css_path)[[1]] 24 | }) 25 | 26 | expect_equal( 27 | output, 28 | ".components-container{display:inline-grid;grid-template-columns:1fr 1fr;width:100%}.components-container .component-box{padding:10px;margin:10px}" # nolint: line_length_linter 29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /vignettes/explanation/application-structure.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Explanation: Application structure" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Explanation: Application structure} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | # Philosophy 11 | 12 | Shiny comes with a powerful 13 | [reactive programming](https://shiny.rstudio.com/articles/reactivity-overview.html) model 14 | and a rich set of functions for creating UI widgets 15 | or custom [HTML structure](https://shiny.rstudio.com/articles/tag-glossary.html). 16 | These features make it possible to quickly build impressive, interactive applications, 17 | but they can also make it harder to test and reuse your code. 18 | 19 | To address this issue, we recommend separating the code that depends on Shiny 20 | from the logic which can be expressed without it. 21 | In our experience, this division is crucial for building robust and maintainable applications. 22 | To support this separation, 23 | Rhino encourages a specific structure for the R sources of your application: 24 | 25 | * `main.R`: The entry point to your application. 26 | * `logic`: Application code independent from Shiny. 27 | * `view`: Shiny modules and related code. 28 | 29 | ## Logic 30 | 31 | Use the `logic` directory for code which can be expressed without Shiny. 32 | 33 | Every Shiny app may have a different end goal, 34 | but they all generally contain isolatable sections of code 35 | that can expressed as a normal R functions. 36 | This could be data manipulation, generating non-interactive plots and graphs, 37 | or connecting to an external data source, 38 | but outside of definable inputs, it doesn't interact with or rely on Shiny in any way. 39 | 40 | Code that relies upon reactivity or UI builder/markup functions 41 | can be problematic to test and difficult to reuse. 42 | With proper design and understanding of this concept, 43 | it is possible to express most of your application logic 44 | using plain R functions and data structures (like lists, data frames). 45 | 46 | ## View 47 | 48 | The `view` directory should contain code which describes the user interface of your application 49 | and relies upon the reactive capabilities of Shiny. 50 | Here is where we will use the functions defined in `logic`, 51 | and where the core app functionality will be defined. 52 | 53 | If you are not familiar with [Shiny modules](https://shiny.rstudio.com/articles/modules.html), 54 | please take the time to read up on the concept. 55 | In short, using modules we can isolate paired Shiny UI/Server code, 56 | and we prevent overlap of reactivity 57 | by wrapping all input/output value names with the `ns()` function. 58 | This allows us to "namespace" the running module 59 | and use it multiple times in the same application. 60 | This is a very important concept to shortly summarize, 61 | but if this is new to you just remember that if you want to reference a UI element in the server, 62 | it needs to be namespaced. 63 | 64 | A typical module could be structured like this: 65 | 66 | ``` r 67 | box::use( 68 | shiny[moduleServer, NS, renderText, tagList, textInput, textOutput], 69 | ) 70 | box::use( 71 | app/logic/messages[hello_message], 72 | ) 73 | 74 | #' @export 75 | ui <- function(id) { 76 | ns <- NS(id) 77 | tagList( 78 | textInput(ns("name"), "Name"), 79 | textOutput(ns("message")) 80 | ) 81 | } 82 | 83 | #' @export 84 | server <- function(id) { 85 | moduleServer(id, function(input, output, session) { 86 | output$message <- renderText(hello_message(input$name)) 87 | }) 88 | } 89 | ``` 90 | 91 | # Minimal `app.R` 92 | 93 | A Rhino application comes with a minimal `app.R`: 94 | 95 | ```r 96 | # Rhino / shinyApp entrypoint. Do not edit. 97 | rhino::app() 98 | ``` 99 | 100 | It is important that you do not edit this file or use it like a `global.R` file, 101 | and instead write your top-level code in `app/main.R`. 102 | It is also important to note that thanks to the `shinyApp` string in the comment, 103 | RStudio recognizes this file as a Shiny application 104 | and displays the "Run" and "Publish" buttons. 105 | 106 | This approach gives Rhino full control over the startup processes of your application. 107 | Steps performed by `rhino::app()` include: 108 | 109 | 1. Purge box cache, so the app can be reloaded without restarting R session. 110 | 2. Configure logger (log level, log file). 111 | 3. Configure static files. 112 | 4. Load the main module / legacy entrypoint. 113 | 5. Add head tags (favicon, CSS & JS). 114 | 115 | It is a fair question to ask if we really need a separate `main.R` file. 116 | Couldn't we just define the top-level `ui` and `server` in `app.R` 117 | and pass it to `rhino::app()` as arguments as we would with a normal `shiny::shinyApp() call`? 118 | 119 | The reasoning behind this structure is to enforce consistent use of the `{box}` modules 120 | throughout the application. 121 | A file loaded with `box::use()` can only load other modules/packages with `box::use()`. 122 | In short, this means that we cannot use the `library()` or `source()` functions in our app. 123 | This is an important distinction from traditional Shiny structure, 124 | where we are simply sourcing `app.R` when the app is loaded. 125 | 126 | As the entire Rhino application is loaded with `box::use(app/main)`, 127 | all its sources must be properly structured as box modules. 128 | -------------------------------------------------------------------------------- /vignettes/explanation/node-js-javascript-and-sass-tools.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Explanation: Node.js - JavaScript and Sass tools" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteEncoding{UTF-8} 6 | %\VignetteIndexEntry{Explanation: Node.js - JavaScript and Sass tools} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | --- 9 | 10 | ## About 11 | 12 | [Node.js](https://nodejs.org/en/about/) is a runtime environment which 13 | can execute JavaScript code outside a web browser. It is used widely for 14 | web development. Its package manager, 15 | [npm](https://docs.npmjs.com/about-npm), makes it easy to install 16 | virtually any JavaScript library. You can use other package managers such as 17 | [bun](https://bun.sh) and [pnpm](https://pnpm.io/) that are compatible with 18 | `npm`. 19 | 20 | To switch from the default npm usage, set a global environment variable named 21 | `RHINO_NPM`. For instance, if you want to use `bun` instead of `npm`, 22 | add `export RHINO_NPM=bun` to your shell startup file (e.g. `.bashrc`). 23 | 24 | Rhino uses Node.js to provide state of the art tools for working with 25 | JavaScript and Sass. The following functions require Node.js to work: 26 | 27 | 1. `build_js()` 28 | 2. `build_sass()` (with `sass: node` configuration in `rhino.yml`) 29 | 3. `lint_js()` 30 | 4. `lint_sass()` 31 | 5. `test_e2e()` 32 | 33 | ### Node directory 34 | 35 | Under the hood Rhino will create a `.rhino` directory in your 36 | project to store the specific libraries needed by these tools. This 37 | directory is git-ignored by default and safe to remove. 38 | 39 | ### Node installation via nvm 40 | 41 | Node can be installed in various ways. One of them relies on 42 | [`nvm`](https://github.com/nvm-sh/nvm) (Node Version Manager). 43 | 44 | There's a known [issue](https://github.com/Appsilon/rhino/issues/345) 45 | when using multiple versions of `Node` that were installed with `nvm` that 46 | causes RStudio to not recognize properly the chosen version. It's caused 47 | by `nvm` and RStudio and can be easily mitigated by starting the RStudio 48 | through the terminal: 49 | 50 | **Ubuntu/Debian** 51 | Open your terminal of choice (i.e. Bash) and run 52 | ``` 53 | rstudio 54 | ``` 55 | 56 | **Windows** 57 | Open your Windows terminal of choice (i.e. Terminal, PowerShell, Git Bash) and 58 | run: 59 | ``` 60 | path/to/your/rstudio/folder/Rstudio.exe 61 | ``` 62 | 63 | **Mac** 64 | Open your Mac terminal of choice (i.e. default Terminal) and run: 65 | ``` 66 | open -na Rstudio 67 | ``` 68 | 69 | ### build_sass() function 70 | 71 | The `build_sass()` function is worth an additional comment. Depending on 72 | the configuration in `rhino.yml` it can use either the 73 | [sass](https://www.npmjs.com/package/sass) Node.js package or the 74 | [sass](https://rstudio.github.io/sass/) R package. We recommend the 75 | Node.js version, as it is the primary, actively developed implementation 76 | of Sass. In contrast, the R package uses the deprecated 77 | [LibSass](https://sass-lang.com/blog/libsass-is-deprecated) 78 | implementation. 79 | -------------------------------------------------------------------------------- /vignettes/explanation/renv-configuration.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Explanation: Renv configuration" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Explanation: Renv configuration} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | This article explains the internals of R dependency management in Rhino. 11 | Practical instructions for adding, removing and updating dependencies 12 | can be found in the documentation of `rhino::pkg_install()` and `rhino::pkg_remove()`. 13 | 14 | Rhino relies on `{renv}` to manage the R package dependencies of your project. 15 | With `{renv}` you can create an isolated package library for each application 16 | and easily restore it on a different machine using the exact same package versions. 17 | This is crucial for the maintainability of any project. 18 | 19 | To learn more about `{renv}` visit its [website](https://rstudio.github.io/renv/index.html). 20 | This article describes the specifics of how Rhino uses `{renv}` 21 | assuming some basic familiarity with the package. 22 | 23 | # Snapshot types 24 | 25 | `{renv}` offers different [snapshot types](https://rstudio.github.io/renv/reference/snapshot.html#snapshot-type). 26 | By default it performs an *implicit* snapshot: 27 | it tries to detect the dependencies of your project by scanning your R sources. 28 | While convenient in small projects, 29 | this approach lacks fine control and can be inefficient in larger code bases. 30 | 31 | It would be preferable to use *explicit* snapshots: 32 | the dependencies of your project must be listed in a `DESCRIPTION` file. 33 | Unfortunately we faced some issues with this snapshot type in deployments. 34 | Instead, Rhino uses the following setup: 35 | 36 | 1. Implicit snapshot (configured in `renv/settings.dcf`). 37 | 1. A `dependencies.R` file with dependencies listed explicitly as `library()` calls. 38 | 1. A `.renvignore` file which tells `{renv}` to only read `dependencies.R`. 39 | 40 | This solution offers us the benefits of explicit snapshots (fine control, efficiency) 41 | and works well in deployment. 42 | 43 | # Manual dependency management 44 | 45 | In most cases the only functions you will need are `rhino::pkg_install()` and `rhino::pkg_remove()`. 46 | However it is still possible to manage dependencies 47 | using the underlying `{renv}` functions directly. 48 | This can be helpful in some unusual situations 49 | (e.g. broken lockfile, installing a specific package version). 50 | 51 | `{renv}` will only save to the lockfile the packages which are installed in the local library, 52 | and it will remove the packages which are not installed. 53 | Thus you should always run `renv::restore(clean = TRUE)` before performing the steps below. 54 | 55 | ## Add a dependency 56 | 57 | 1. Add a `library(package)` line to `dependencies.R`. 58 | 1. Call `renv::install("package")`. 59 | 1. Call `renv::snapshot()`. 60 | 61 | ## Update a dependency 62 | 63 | 1. Call `renv::update("package")`. 64 | 1. Call `renv::snapshot()`. 65 | 66 | Calling `renv::install("package")` instead of `renv::update("package")` will have the same effect. 67 | 68 | ## Remove a dependency 69 | 70 | 1. Remove the `library(package)` line from `dependencies.R`. 71 | 1. Call `renv::snapshot()`. 72 | 1. Call `renv::restore(clean = TRUE)`. 73 | 74 | It is not recommended to use the `renv::remove()` function, 75 | as it will remove a package from the local library even if it is still required by other packages. 76 | For example, `renv::remove("glue")` followed by `renv::snapshot()` 77 | will leave you without the `{glue}` package in your lockfile, 78 | even though it is required by `{shiny}`. 79 | -------------------------------------------------------------------------------- /vignettes/explanation/rhino-style-guide.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Explanation: Rhino style guide" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Explanation: Rhino style guide} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Rhino follows the [`tidyverse` style guide](https://style.tidyverse.org/), 11 | with specific additional rules for `box::use` statements to enhance readability and maintainability of code. 12 | These rules are designed to work alongside `tidyverse` conventions, providing clarity and consistency when using `box` modules. 13 | 14 | For more details on how to use `box::use` statements, see [Explanation: Box modules](https://appsilon.github.io/rhino/articles/explanation/box-modules.html). 15 | 16 | For more details on how to configure linter rules in the `.lintr` file, see [Configuring linters](https://lintr.r-lib.org/articles/lintr.html#configuring-linters). 17 | 18 | # Explicit Import 19 | 20 | For clarity and ease of tracking function origins, avoid using `[...]` for imports. Explicitly declare all packages, modules and functions. 21 | 22 | ```r 23 | # Good 24 | box::use( 25 | infer[specify], 26 | shiny, 27 | ) 28 | 29 | # Bad 30 | box::use( 31 | infer[...], 32 | shiny[...], 33 | ) 34 | 35 | observe() # Is it from {infer} or {shiny}? 36 | ``` 37 | 38 | # Trailing Commas 39 | 40 | Trailing commas in `box::use` statements are encouraged. They simplify line additions and reordering. 41 | 42 | ```r 43 | # Good 44 | box::use( 45 | shiny, 46 | ) 47 | 48 | # Bad 49 | box::use( 50 | shiny 51 | ) 52 | ``` 53 | 54 | # Separated Statements for Packages and Modules 55 | 56 | Use separate `box::use` statements for importing packages and modules (R scripts) for better structure and readability. 57 | 58 | ```r 59 | # Good 60 | box::use( 61 | rhino[log], 62 | shiny, 63 | ) 64 | 65 | box::use( 66 | path/to/module, 67 | ) 68 | 69 | # Bad 70 | box::use( 71 | rhino[log], 72 | shiny, 73 | path/to/module, 74 | ) 75 | ``` 76 | 77 | # Order of Imports 78 | 79 | Order imports alphabetically to ease locating a specific import. This applies to both packages/modules and functions within them. 80 | 81 | ```r 82 | # Good 83 | box::use( 84 | rhino, 85 | shiny[div, fluidPage], 86 | ) 87 | 88 | # Bad 89 | box::use( 90 | shiny[fluidPage, div], 91 | rhino, 92 | ) 93 | ``` 94 | 95 | ## Aliases 96 | 97 | Aliases can be useful for long package/module and function names. Imports should still follow the alphabetical order of package/module names and function names. 98 | 99 | ```r 100 | # Good 101 | box::use( 102 | z_pkg = rhino, 103 | shiny[div, a_fun = fluidPage], 104 | ) 105 | 106 | # Bad 107 | box::use( 108 | a_pkg = shiny, 109 | rhino[a_fun = react_component, log], 110 | ) 111 | ``` 112 | 113 | # Number of Imports 114 | 115 | Limit the number of functions imported from a module or package to 8. 116 | If more than 8 functions are needed, import the entire package and reference functions using `package$function`. 117 | Aliases can be used for convenience. Check [`box::use` documentation](https://klmr.me/box/reference/use.html) for more details. 118 | 119 | ```r 120 | # Good 121 | box::use( 122 | rhino[log], 123 | shiny, 124 | ) 125 | 126 | # Bad 127 | box::use( 128 | rhino[log], 129 | shiny[div, fluidPage, navbarPage, sidebarPanel, sidebarLayout, mainPanel, tabPanel, tabsetPanel, titlePanel], 130 | ) 131 | ``` 132 | 133 | # Automated Styling of `box::use()` calls 134 | 135 | As of Rhino version 1.10.0, `format_r()` includes styling for `box::use()` calls. This is provided by [`{box.linters}`](https://appsilon.github.io/box.linters/) version >= 0.10.4. As with [`{styler}`](https://styler.r-lib.org/), carefully examine the styling results after performing automated styling on your code. 136 | 137 | Automated styling covers some of the topics described above such as: 138 | 139 | * Separating `box::use()` calls for packages and local modules 140 | * Alphabetically sorting packages, modules, and functions. 141 | * Adding trailing commas 142 | 143 | ```r 144 | # Original 145 | box::use(stringr[str_trim, str_pad], dplyr, app/logic/table) 146 | 147 | # Styled 148 | box::use( 149 | dplyr, 150 | stringr[str_pad, str_trim], 151 | ) 152 | 153 | box::use( 154 | app/logic/table 155 | ) 156 | ``` 157 | 158 | ## Requirements 159 | 160 | * R version >= 4.3.0 161 | * `box.linters` version >= 0.10.4 162 | * `treesitter` package 163 | * `treesitter.r` package 164 | 165 | ## How to use 166 | 167 | 1. `box::use()` call styling is included when running `format_r()`. 168 | 2. It can also be separately executed by running one of the [`box.linters::style_*`](https://appsilon.github.io/box.linters/reference/index.html#box-styling) functions. 169 | 170 | For more information on the abilities of `{{box.linters}}` styling functions refer to the styling functions [documentation](https://appsilon.github.io/box.linters/reference/style_box_use_text.html). 171 | -------------------------------------------------------------------------------- /vignettes/explanation/rhino-yml.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Explanation: Configuring Rhino - rhino.yml" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Explanation: Configuring Rhino - rhino.yml} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | # Configure Rhino with `rhino.yml` 11 | 12 | Rhino uses its own `rhino.yml` config file where you can set a few options on how it works in your app. Currently available 13 | options are described below. 14 | 15 | ## `rhino.yml` options 16 | 17 | ```yaml 18 | sass: string # required | one of: "node", "r", "custom" 19 | legacy_entrypoint: string # optional | one of: "app_dir", "source", "box_top_level" 20 | ``` 21 | 22 | ### `sass` 23 | 24 | This option controls the behavior `rhino::build_sass()`: 25 | 26 | * `node`: Build Sass using the [Node.js package](https://www.npmjs.com/package/sass). 27 | * `r`: Build Sass using the [R package](https://cran.r-project.org/package=sass). 28 | * `custom`: Do nothing. Useful when [bundling custom Sass with `bslib` theme](https://appsilon.github.io/rhino/articles/how-to/use-bslib.html). 29 | 30 | Read more in [Explanation: Node.js - JavaScript and Sass tools](https://appsilon.github.io/rhino/articles/explanation/node-js-javascript-and-sass-tools.html). 31 | 32 | ### `legacy_entrypoint` 33 | 34 | This setting is useful when migrating an existing Shiny application to Rhino. For more details see 35 | [`rhino::app()` details section](https://appsilon.github.io/rhino/reference/app.html#details-1). 36 | -------------------------------------------------------------------------------- /vignettes/explanation/what-is-rhino.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Explanation: What is Rhino?" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Explanation: What is Rhino?} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | # What is Rhino? 11 | 12 | Rhino is an R package designed to help you build high quality, enterprise-grade Shiny applications at speed. It allows you to create Shiny apps "The Appsilon Way" - like a fullstack software engineer: apply best software engineering practices, modularize your code, test it well, make UI beautiful and think about adoption from the very beginning. 13 | 14 | Rhino is an opinionated framework with a focus on best practices and development tools. 15 | It started as a series internal projects at Appsilon aiming to: 16 | 17 | 1. Save time and avoid repetitive tasks: include all best practices we care about from the very beginning of a project. 18 | 2. Unify applications' architecture: provide sensible defaults so that we don't reinvent the wheel. 19 | 3. Automate and codify our existing practices: pass knowledge in the form of code instead of documents and manuals. 20 | 21 | Over the past few years, we have been building internal tools to address these issues and help us easily structure projects in a fast way. It has since evolved into an R package that we are now excited to share with the Shiny community. 22 | 23 | Please keep in mind that this project is in the early stages. We wanted to get something out to the R community and look forward to continuing development with feedback from users. This is just the beginning. 24 | 25 | # Why Rhino? 26 | 27 | Because Rhino helps you build Shiny apps faster, while making them more reliable and easier to maintain. It bundles in a coherent way a set of tools and practices that are beneficial for most Shiny applications, especially in enterprise. 28 | 29 | You may want to use Rhino if: 30 | 31 | 1. You need a nested files structure that will handle a bigger application. 32 | 2. You want to follow a complete set of solutions built on industry experience, avoid spending time "reinventing the wheel". 33 | 3. You'd like to have a scalable, modularized application with clear code organization and neat separation of responsibilities. Rhino can serve as a guide to understanding these concepts (box, Shiny modules, view / logic separation). 34 | 4. You want to save time and avoid repetitive tasks. Rhino allows you to quickly start your Shiny project with a set of preconfigured development tools (linters, CI, Cypress, logging, Sass and JS building) 35 | 5. You are building an application for production use in enterprise - you need to make sure it's highly maintainable and reliable in the long term. Most Shiny applications can be converted to a Rhino project in less than 2 hours. 36 | 37 | # Similar projects 38 | 39 | Rhino is not the first project of its kind aimed at helping the Shiny community to enhance the structure of their applications. We believe that each of these has value, and it is up to the developer to decide what is best for them in their project. 40 | 41 | How Rhino is different from ...? 42 | 43 | * **golem:** Rhino apps are not R packages. Rhino puts more emphasis on development tools, clean configuration and minimal boilerplate and tries to provide default solutions for typical problems and questions in these areas. 44 | * **leprechaun:** Leprechaun works by scaffolding Shiny apps, without adding dependencies. Rhino minimizes generated code and aims to provide a complete foundation for building Shiny apps ready for deployment in enterprise, so that you can focus on application's logic and user experience. 45 | * **devtools:** devtools streamlines packages development. Rhino is a complete framework for building Shiny apps. Rhino features are interdependent (e.g. coverage and unit tests) and cannot be used without making the app into basic Rhino structure. 46 | * **usethis:** usethis adds independent code snippets you ask it to. Rhino is a complete framework for building Shiny apps. Your app is designed to call Rhino functions instead of having them insert code into your project. 47 | -------------------------------------------------------------------------------- /vignettes/how-to/add-internationalization.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Add internationalization" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Add internationalization} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Internationalization can be introduced in a Rhino application 11 | using [`shiny.i18n` package](https://appsilon.github.io/shiny.i18n/). 12 | 13 | You can achieve that by creating an instance of the `shiny.i18n::Translator` class, 14 | providing translations as JSON or CSV files 15 | and wrapping parts of your application that need translating in the `translate` method. 16 | 17 | A detailed tutorial on how to apply `shiny.i18n` in Rhino applications 18 | can be found [here](https://appsilon.github.io/shiny.i18n/articles/rhino.html). 19 | -------------------------------------------------------------------------------- /vignettes/how-to/add-routing.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Add routing" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Add routing} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | In the Rhino application, routing can be introduced using 11 | [`shiny.router` package](https://appsilon.github.io/shiny.router/). 12 | A detailed tutorial on how to apply it in Rhino applications 13 | can be found [here](https://appsilon.github.io/shiny.router/articles/rhino.html). 14 | -------------------------------------------------------------------------------- /vignettes/how-to/box-lsp.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Auto-complete in VSCode" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Auto-complete in VSCode} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | _Box-module auto-complete only works if one uses VSCode or Vim. It does not have any known adverse effects on RStudio Desktop or Posit Workbench._ 11 | 12 | # Introduction 13 | 14 | Rhino utilizes `{box}` modules to manage large code bases. A disadvantage of `{box}` modules is the lack of syntax auto-complete support in RStudio or VSCode. In VSCode, auto-complete is provided by `{languageserver}` which allows for external parsers. [`{box.lsp}`](https://appsilon.github.io/box.lsp/index.html) is an extension to `{languageserver}` to provide function name and function argument auto-complete support for `{box}` modules. Please refer to the `{box.lsp}` [documentation](https://appsilon.github.io/box.lsp/index.html) for its current capabilities. 15 | 16 | # Steps 17 | 18 | 1. Install `languageserver`: `renv::install("languageserver")`. `languageserver` is a developer tool, it is _not_ advised to add it to `dependencies.R` or to `renv.lock`. 19 | 2. If you initialized your Rhino app with version 1.10.0 or later, proceed to step 4. 20 | 3. If you are using an existing Rhino app with version < 1.10.0: 21 | 1. Install `box.lsp` to project dependencies: `rhino::pkg_install("box.lsp")`. 22 | 2. Run [`box.lsp::use_box_lsp()`](https://appsilon.github.io/box.lsp/reference/use_box_lsp.html) to update your project's `.Rprofile` file with `box.lsp` configuration. 23 | 4. Restart the R session (restart VSCode) to reload `.Rprofile`. 24 | -------------------------------------------------------------------------------- /vignettes/how-to/build-rhino-apps-with-llm-tools.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Build Rhino apps with LLM tools" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Build Rhino apps with LLM tools} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | LLM tools like [GitHub Copilot](https://github.com/features/copilot) can be extremely helpful when building apps with Rhino. 11 | However, the unique project structure and the use of the box package for module imports can make it harder for these tools to understand and assist effectively. 12 | The good news is that their performance can be significantly improved by providing custom instructions. 13 | 14 | # Custom instructions for GitHub Copilot 15 | 16 | To optimize GitHub Copilot for working with Rhino projects, 17 | you can create a file named `copilot-instructions.md` in the `.github/` directory of your repository. 18 | To see instructions below as markdown [visit vignette source](https://github.com/Appsilon/rhino/blob/main/vignettes/how-to/build-rhino-apps-with-llm-tools.Rmd). 19 | 20 | Example instructions: 21 | 22 |
23 | ## Importing and exporting 24 | 25 | Use only `box::use` for imports. Using `library` and `::` is forbidden. 26 | 27 | `box::use` statement (if needed) should be located at the top of the file. 28 | 29 | There can be two `box::use` statements per file. First one should include only R packages, second should only import other scripts. 30 | 31 | Imports in `box::use` should be sorted alphabetically. 32 | 33 | Using `[...]` is forbidden. 34 | 35 | All external functions in a script should be imported. This includes operators, like `%>%`. 36 | 37 | A script should only import functions that it uses. 38 | 39 | ### Ways of importing 40 | 41 | There are two ways a package or a script can be imported: 42 | 43 | 1. List imported functions - functions imported are listed in [] 44 | 45 | ```r 46 | box::use( 47 | dplyr[filter], 48 | ) 49 | 50 | filter(mtcars, cyl > 4) 51 | ``` 52 | 53 | Use it if there are no more than 8 functions imported from this package/script. 54 | 55 | 2. Import package and access functions with `$` 56 | 57 | ```r 58 | box::use( 59 | dplyr, 60 | ) 61 | dplyr$filter(mtcars, cyl > 4) 62 | ``` 63 | 64 | When moving function into a different script, remember to adjust imports in `box::use`: 65 | 66 | 1. Add import for all required functions to the file where you moved the function. 67 | 2. Make sure to follow the correct way of importing (direct or using $) in the new file. Modify it if needed. 68 | 3. Remove redundant imports from the original file. 69 | 4. Import the moved function in the original file. 70 | 71 | Use it if there are more than 8 functions imported from this package/script. 72 | 73 | ### Exporting 74 | 75 | If a function is used only inside a script, it should not be exported. 76 | 77 | If a function is used by other scripts, it should be exported by adding `#' @export` before the function. 78 | 79 | ## Rhino modules 80 | 81 | When creating a new module in `app/view`, use the template: 82 | ```r 83 | box::use( 84 | shiny[moduleServer, NS] 85 | ) 86 | 87 | #' @export 88 | ui <- function(id) { 89 | ns <- NS(id) 90 | 91 | } 92 | 93 | #' @export 94 | server <- function(id) { 95 | moduleServer(id, function(input, output, session) { 96 | 97 | }) 98 | } 99 | ``` 100 | 101 | ## Unit tests 102 | 103 | All R unit tests are located in `tests/testthat`. 104 | 105 | There should be only one test file per script, named `test-{script name}.R`. 106 | 107 | If testing private functions (ones that are not exported), use this pattern: 108 | 109 | ```r 110 | box::use(app/logic/mymod) 111 | 112 | impl <- attr(mymod, "namespace") 113 | 114 | test_that('{test description}', { 115 | expect_true(impl$this_works()) 116 | }) 117 | ``` 118 | 119 | ### Testing exported and non-exported functions 120 | 121 | When testing a box module that contains both exported and non-exported functions: 122 | 123 | 1. Import the entire module without specifying individual functions: 124 | 125 | ```r 126 | box::use( 127 | app/logic/mymodule, 128 | ) 129 | ``` 130 | 131 | 2. Access exported functions using the module name with `$`: 132 | 133 | ```r 134 | test_that("exported function works", { 135 | expect_equal(mymodule$exported_function(1), 2) 136 | }) 137 | ``` 138 | 139 | 3. For testing non-exported functions, get the module's namespace at the start of the test file: 140 | 141 | ```r 142 | impl <- attr(mymodule, "namespace") 143 | 144 | test_that("non-exported function works", { 145 | expect_equal(impl$internal_function(1), 2) 146 | }) 147 | ``` 148 | 149 | This pattern allows testing both public and private functions while maintaining proper encapsulation. 150 | 151 | ## Code style 152 | 153 | The maximum line length is 100 characters. 154 |
155 | -------------------------------------------------------------------------------- /vignettes/how-to/enable-shiny-bookmarking.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Enable Shiny bookmarking" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Enable Shiny bookmarking} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | To use Shiny [bookmarking](https://shiny.rstudio.com/articles/bookmarking-state.html), 11 | call `shiny::enableBookmarking()` somewhere in your `main.R`: 12 | 13 | ```r 14 | box::use( 15 | shiny, 16 | ) 17 | 18 | shiny$enableBookmarking() 19 | 20 | #' @export 21 | ui <- function(id) { 22 | ns <- shiny$NS(id) 23 | shiny$bootstrapPage( 24 | shiny$bookmarkButton(), 25 | shiny$textInput(ns("name"), "Name"), 26 | shiny$textOutput(ns("message")) 27 | ) 28 | } 29 | 30 | #' @export 31 | server <- function(id) { 32 | shiny$moduleServer(id, function(input, output, session) { 33 | output$message <- shiny$renderText(paste0("Hello ", input$name, "!")) 34 | }) 35 | } 36 | ``` 37 | 38 | If you are using a [legacy entrypoint](https://appsilon.github.io/rhino/reference/app.html#legacy-entrypoint), 39 | make sure that your UI is a function 40 | as described in the details section of `shiny::enableBookmarking()`. 41 | 42 | For example, with `legacy_entrypoint: source` in `rhino.yml` you might use: 43 | ```r 44 | ui <- function(request) { 45 | bootstrapPage( 46 | bookmarkButton(), 47 | textField("text", "Text") 48 | ) 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /vignettes/how-to/images/communicate_between_modules_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/how-to/images/communicate_between_modules_1.png -------------------------------------------------------------------------------- /vignettes/how-to/images/communicate_between_modules_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/how-to/images/communicate_between_modules_2.png -------------------------------------------------------------------------------- /vignettes/how-to/images/rhino_addins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/how-to/images/rhino_addins.png -------------------------------------------------------------------------------- /vignettes/how-to/keep-multiple-apps-in-a-single-repository.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How to: Keep multiple apps in a single repository" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How to: Keep multiple apps in a single repository} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | In some cases you might want to put your Rhino application in a subdirectory of your repository - 11 | for example, to keep multiple apps in a single repository. 12 | Most of Rhino features will work just fine, but CI will need some adjustment. 13 | 14 | Rhino comes with a CI workflow (Continuous Integration) 15 | for [GitHub Actions](https://docs.github.com/en/actions) 16 | which automatically runs linters and tests whenever you push to the repository. 17 | It will work out of box if you application lives in the root of your git repository. 18 | You will need some manual tweaks if your application lives in a subdirectory: 19 | 20 | 1. Place all workflows in the `.github/workflows` directory. 21 | 2. Adjust each workflow by setting 22 | a [default working directory](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iddefaultsrun) 23 | accordingly. 24 | -------------------------------------------------------------------------------- /vignettes/how-to/manage-secrets-and-environments.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How to: Manage secrets and environments" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How to: Manage secrets and environments} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Managing secrets and configuration can be a challenge 11 | in applications which need to work in different development and deployment environments. 12 | Typically, secrets are credentials for an external service such as a database. 13 | Environments, on the other hand, are used to manage multiple configurations that we can be easily switched between. 14 | Rhino recommends a way for working with either one. 15 | 16 | # Secrets 17 | Secrets are a confidential information and should not be tracked in your version control system. 18 | Therefore, a natural place for them are system environment variables. 19 | Variables set in system environment can be retrieved within your code with `Sys.getenv()`. 20 | 21 | R provides a way to easily set environment variables. 22 | Upon a session start (or restart) R reads `.Renviron` file contents and sets environment variables. 23 | 24 | **.Renviron** 25 | ```sh 26 | # A comment in .Renviron file 27 | DATABASE_PASSWORD="foobar123!" 28 | API_KEY="75170fc230cd88f32e475ff4087f81d9" 29 | ``` 30 | 31 | Secrets defined via environment variables can be read and used the following way: 32 | ```r 33 | db_password <- Sys.getenv("DATABASE_PASSWORD") 34 | if (db_password == "") { 35 | # Handle unset or empty DATABASE_PASSWORD variable 36 | } 37 | pool <- pool::dbPool( 38 | drv = RMySQL::MySQL(), 39 | dbname = "...", 40 | host = "...", 41 | username = "admin", 42 | password = db_password 43 | ) 44 | ``` 45 | 46 | ## Recommendations for storing secrets 47 | 48 | 1. Store secrets and environment variables in `.Renviron`. 49 | 1. Use a separate `.Renviron` file for every environment. Swap the whole file when changing environments. 50 | 1. Use `CONSTANT_CASE` for variable names. 51 | 1. Do not track `.Renviron` file in a version control system. Store it in a secure location, e.g. a password manager. 52 | 1. Do not publish `.Renviron` to RStudio Connect nor [shinyapps.io](https://www.shinyapps.io/). Both, RStudio Connect and Shiny Apps, provide means to manage environment variables. 53 | 54 | # Environments 55 | 56 | Having every configurable setting stored as an environment variable would result in overgrown `.Renviron` files. 57 | That's where configurable environments come in. 58 | 59 | Everything that is not confidential can be tracked by a version control system. 60 | Rhino endorses use of `{config}` package for managing environments. 61 | 62 | **config.yml** 63 | ```yml 64 | default: 65 | rhino_log_level: !expr Sys.getenv("RHINO_LOG_LEVEL", "INFO") 66 | rhino_log_file: !expr Sys.getenv("RHINO_LOG_FILE", NA) 67 | database_user: "service_account" 68 | database_schema: "dev" 69 | 70 | dev: 71 | rhino_log_level: !expr Sys.getenv("RHINO_LOG_LEVEL", "DEBUG") 72 | 73 | staging: 74 | database_schema: "stg" 75 | 76 | production: 77 | database_user: "service_account_prod" 78 | database_schema: "prod" 79 | ``` 80 | 81 | **.Renviron** 82 | ```sh 83 | R_CONFIG_ACTIVE="dev" 84 | ``` 85 | 86 | You can access the configuration variables in the following way: 87 | ```r 88 | box::use(config) 89 | 90 | config$get("rhino_log_level") # == "DEBUG" 91 | config$get("database_user") # == "service_account" 92 | 93 | config$get("rhino_log_level", config = "production") # == "INFO" 94 | config$get("database_user", config = "production") # == "service_account_prod" 95 | 96 | withr::with_envvar(list(RHINO_LOG_LEVEL = "ERROR"), { 97 | config$get("rhino_log_level") # == "ERROR" 98 | config$get("rhino_log_level", config = "production") # == "ERROR" 99 | }) 100 | ``` 101 | 102 | ## Recommendations for managing environments 103 | 104 | 1. Define environments and their settings in `config.yml`. 105 | 1. Select config by setting `R_CONFIG_ACTIVE` variable in `.Renviron`. 106 | 1. Make use of default values. 107 | 1. Use `!expr Sys.getenv()` to make settings overridable with environment variables. 108 | 1. Import config with box and call as usual, i.e. `box::use(config)` and `config$get()`. 109 | 1. Use `snake_case` for field names. 110 | -------------------------------------------------------------------------------- /vignettes/how-to/migrate-1-10.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Rhino 1.10 Migration Guide" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Rhino 1.10 Migration Guide} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Follow the steps outlined in this guide to migrate your project to Rhino 1.10. 11 | Before starting, ensure your Git working tree is clean, or back up your project if not using Git. 12 | 13 | This guide assumes you are migrating from Rhino 1.9. 14 | If you are currently using an older version of Rhino, 15 | please review the older migration guides first: 16 | 17 | * [Rhino 1.6 Migration Guide](https://appsilon.github.io/rhino/articles/how-to/migrate-1-6.html). 18 | * [Rhino 1.7 Migration Guide](https://appsilon.github.io/rhino/articles/how-to/migrate-1-7.html). 19 | * [Rhino 1.8 Migration Guide](https://appsilon.github.io/rhino/articles/how-to/migrate-1-8.html). 20 | * [Rhino 1.9 Migration Guide](https://appsilon.github.io/rhino/articles/how-to/migrate-1-9.html). 21 | 22 | # Step 1: Install Rhino 1.10 23 | 24 | Use the following command to install Rhino 1.10 and update your `renv.lock` file: 25 | 26 | ```r 27 | rhino::pkg_install("rhino@1.10.0") 28 | ``` 29 | 30 | After the installation, restart your R session to ensure all changes take effect. 31 | 32 | # Step 2: Update your .Rprofile 33 | 34 | Update your `.Rprofile` with `languageserver` parsers for `box::use`, by running: 35 | 36 | ```r 37 | box.lsp::use_box_lsp() 38 | ``` 39 | 40 | Restart your R session so changes can take effect 41 | 42 | Follow ["How-to: Auto-complete in VSCode" guide]() to enable auto-complete in VSCode or Vim. 43 | 44 | # Step 3 (optional, requires R >= 4.3): Install `treesitter` and `treesitter.r` 45 | 46 | To enable automated styling of `box::use` statements and `box.linters::namespace_function_calls()` linter, 47 | install `treesitter` and `treesitter.r`: 48 | 49 | ```r 50 | rhino::pkg_install(c("treesitter", "treesitter.r")) 51 | ``` 52 | 53 | # Step 4: Test your project 54 | 55 | Test your project thoroughly to ensure everything works properly after the migration. 56 | If you encounter any issues or have further questions, 57 | don't hesitate to reach out to us via 58 | [GitHub Discussions](https://github.com/Appsilon/rhino/discussions). 59 | -------------------------------------------------------------------------------- /vignettes/how-to/migrate-1-6.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Rhino 1.6 Migration Guide" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Rhino 1.6 Migration Guide} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Transition your project to Rhino version 1.6 with this comprehensive guide. The latest version includes Node module updates to enhance your development workflow. 11 | 12 | # Prerequisites 13 | 14 | - Back up your project data. 15 | - Ensure Node.js is up-to-date on your machine. 16 | 17 | # Installation of Rhino 1.6 18 | 19 | Choose one of the following methods to install Rhino 1.6: 20 | 21 | ## Option 1: Using `renv` 22 | 23 | Install Rhino using renv and then take a snapshot of your project dependencies: 24 | 25 | ```r 26 | renv::install("rhino") 27 | renv::snapshot() 28 | ``` 29 | 30 | ## Option 2: Using `rhino::pkg_install` (for Rhino v1.4+) 31 | 32 | For newer versions of Rhino, you can use the built-in package installation function: 33 | 34 | ```r 35 | rhino::pkg_install("rhino") 36 | ``` 37 | 38 | After the installation, restart your R session to ensure all changes take effect. 39 | 40 | # Migration Steps 41 | 42 | ## Step 1: Remove the `.rhino` Directory 43 | 44 | Locate and remove the `.rhino` directory from the root of your project. This directory contains configuration settings from the previous version of Rhino. 45 | 46 | ```bash 47 | rm -rf .rhino 48 | ``` 49 | 50 | ## Step 2: Run Node Tool Functions 51 | 52 | Invoke one of the following commands to run Node tools. This action will regenerate the `.rhino` directory with a new configuration, including updated Node modules. 53 | 54 | ```r 55 | rhino::build_sass() 56 | rhino::lint_sass() 57 | rhino::build_js() 58 | rhino::lint_js() 59 | ``` 60 | 61 | ## Step 3: Migrate Cypress End-to-End Tests 62 | 63 | If your project includes Cypress end-to-end tests, initiate the migration wizard with: 64 | 65 | ```r 66 | rhino::test_e2e(interactive = TRUE) 67 | ``` 68 | 69 | Follow the prompts in the migration wizard to update your end-to-end tests. 70 | 71 | ## Step 4: Test Your Project 72 | 73 | Conduct extensive testing to confirm that all components of your project function properly after the migration. 74 | 75 | # Final Steps 76 | 77 | If you encounter any issues or have further questions after migrating to Rhino 1.6, please consult the GitHub discussions for Rhino for community and developer support. 78 | -------------------------------------------------------------------------------- /vignettes/how-to/migrate-1-7.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Rhino 1.7 Migration Guide" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Rhino 1.7 Migration Guide} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Follow the steps outlined in this guide to migrate your project to Rhino 1.7. 11 | Before starting, ensure your Git working tree is clean, or back up your project if not using Git. 12 | 13 | This guide assumes you are migrating from Rhino 1.6. 14 | If you are currently using an older version of Rhino, 15 | please start with 16 | [Rhino 1.6 Migration Guide](https://appsilon.github.io/rhino/articles/how-to/migrate-1-6.html). 17 | 18 | # Step 1: Install Rhino 1.7 19 | 20 | Use the following command to install Rhino 1.7 and update your `renv.lock` file: 21 | 22 | ```r 23 | rhino::pkg_install("rhino@1.7.0") 24 | ``` 25 | 26 | After the installation, restart your R session to ensure all changes take effect. 27 | 28 | # Step 2: Update your linter rules 29 | 30 | Edit the `.lintr` file in your project so it includes the following rules: 31 | 32 | ```r 33 | linters: 34 | linters_with_defaults( 35 | box_func_import_count_linter = rhino::box_func_import_count_linter(), 36 | box_separate_calls_linter = rhino::box_separate_calls_linter(), 37 | box_trailing_commas_linter = rhino::box_trailing_commas_linter(), 38 | box_universal_import_linter = rhino::box_universal_import_linter(), 39 | line_length_linter = line_length_linter(100), 40 | object_usage_linter = NULL # Does not work with `box::use()`. 41 | ) 42 | ``` 43 | 44 | # Step 3: Test your project 45 | 46 | Test your project thoroughly to ensure everything works properly after the migration. 47 | In particular, run `rhino::lint_r()` and fix the problems it reports. 48 | 49 | If you encounter any issues or have further questions, 50 | don't hesitate to reach out to us via 51 | [GitHub Discussions](https://github.com/Appsilon/rhino/discussions). 52 | -------------------------------------------------------------------------------- /vignettes/how-to/migrate-1-8.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Rhino 1.8 Migration Guide" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Rhino 1.8 Migration Guide} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Follow the steps outlined in this guide to migrate your project to Rhino 1.8. 11 | Before starting, ensure your Git working tree is clean, or back up your project if not using Git. 12 | 13 | This guide assumes you are migrating from Rhino 1.6 or 1.7. 14 | If you are currently using an older version of Rhino, 15 | please start with 16 | [Rhino 1.6 Migration Guide](https://appsilon.github.io/rhino/articles/how-to/migrate-1-6.html). 17 | 18 | # Step 1: Install Rhino 1.8 19 | 20 | Use the following command to install Rhino 1.8 and update your `renv.lock` file: 21 | 22 | ```r 23 | rhino::pkg_install("rhino@1.8.0") 24 | ``` 25 | 26 | After the installation, restart your R session to ensure all changes take effect. 27 | 28 | # Step 2: Update your linter rules 29 | 30 | ## Option A: You use `.lintr` provided by Rhino without modifications 31 | 32 | Run the following command to replace your `.lintr` file with the new default one provided by Rhino: 33 | 34 | ```r 35 | box.linters::use_box_lintr(type = "rhino") 36 | ``` 37 | 38 | ## Option B: You have customized `.lintr` file 39 | 40 | Edit the `.lintr` file in your project so it uses `box.linters::rhino_default_linters` as the default linters: 41 | 42 | ```r 43 | linters: 44 | linters_with_defaults( 45 | defaults = box.linters::rhino_default_linters, 46 | line_length_linter = lintr::line_length_linter(100) # You can add your custom linters here 47 | ) 48 | ``` 49 | 50 | # Step 3: Update GitHub Actions workflow 51 | 52 | _Note: This step is only necessary if you are using GitHub Actions in your project._ 53 | 54 | To update your workflow, run: 55 | ```r 56 | file.copy( 57 | system.file("templates", "github_ci", "dot.github", "workflows", "rhino-test.yml", package = "rhino"), 58 | file.path(".github", "workflows", "rhino-test.yml") 59 | ) 60 | ``` 61 | 62 | This command will replace the current GitHub Actions workflow with the new Rhino-provided one. 63 | If you have customized your workflow, you will need to manually update it to include the new triggers added to the template. 64 | The changes can be found in [this commit](https://github.com/Appsilon/rhino/commit/8e080655f81865a30af51330cd81f4614d3a7405). 65 | 66 | # Step 4: Test your project 67 | 68 | Test your project thoroughly to ensure everything works properly after the migration. 69 | In particular, run `rhino::lint_r()` and fix the problems it reports. 70 | 71 | If you encounter any issues or have further questions, 72 | don't hesitate to reach out to us via 73 | [GitHub Discussions](https://github.com/Appsilon/rhino/discussions). 74 | -------------------------------------------------------------------------------- /vignettes/how-to/migrate-1-9.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Rhino 1.9 Migration Guide" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Rhino 1.9 Migration Guide} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Follow the steps outlined in this guide to migrate your project to Rhino 1.9. 11 | Before starting, ensure your Git working tree is clean, or back up your project if not using Git. 12 | 13 | This guide assumes you are migrating from Rhino 1.8. 14 | If you are currently using an older version of Rhino, 15 | please review the older migration guides first: 16 | 17 | * [Rhino 1.6 Migration Guide](https://appsilon.github.io/rhino/articles/how-to/migrate-1-6.html). 18 | * [Rhino 1.7 Migration Guide](https://appsilon.github.io/rhino/articles/how-to/migrate-1-7.html). 19 | * [Rhino 1.8 Migration Guide](https://appsilon.github.io/rhino/articles/how-to/migrate-1-8.html). 20 | 21 | # Step 1: Install Rhino 1.9 22 | 23 | Use the following command to install Rhino 1.9 and update your `renv.lock` file: 24 | 25 | ```r 26 | rhino::pkg_install("rhino@1.9.0") 27 | ``` 28 | 29 | After the installation, restart your R session to ensure all changes take effect. 30 | 31 | # Step 2: Remove the `.rhino` directory 32 | 33 | Remove the `.rhino` directory from the root of your project. 34 | This directory contains Node.js configuration from the previous version of Rhino. 35 | 36 | ```r 37 | unlink(".rhino", recursive = TRUE) 38 | ``` 39 | 40 | # Step 3: Test your project 41 | 42 | Test your project thoroughly to ensure everything works properly after the migration. 43 | If you encounter any issues or have further questions, 44 | don't hesitate to reach out to us via 45 | [GitHub Discussions](https://github.com/Appsilon/rhino/discussions). 46 | -------------------------------------------------------------------------------- /vignettes/how-to/publish-on-huggingface.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Publish Rhino app on Hugging Face" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Publish Rhino app on Hugging Face} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | [Hugging Face](https://huggingface.co/) is a platform that allows publishing models, datasets, and web applications. 11 | Shiny applications (including Rhino) also can be hosted this way, with Hugging Face support for Docker containers as SDK. 12 | A sample Rhino application can be found [here](https://huggingface.co/spaces/Appsilon/shiny-for-r-rhino). 13 | 14 | To publish your application, you will need to provide a `Dockerfile` that defines the container that will be used to host it. 15 | Such a `Dockerfile` can look like this: 16 | 17 | ```dockerfile 18 | FROM rocker/shiny-verse:4.3.0 19 | 20 | # Workaround for renv cache 21 | RUN mkdir /.cache 22 | RUN chmod 777 /.cache 23 | 24 | WORKDIR /code 25 | 26 | # Install renv 27 | RUN install2.r --error \ 28 | renv 29 | 30 | # Copy application code 31 | COPY . . 32 | 33 | # Install dependencies 34 | RUN Rscript -e 'options(renv.config.cache.enabled = FALSE); renv::restore(prompt = FALSE)' 35 | 36 | CMD ["R", "--quiet", "-e", "shiny::runApp(host='0.0.0.0', port=7860)"] 37 | ``` 38 | 39 | You might need to modify it with e.g. different R version, base Docker image, or some additional dependencies. 40 | Check [Docker documentation](https://docs.docker.com/engine/reference/builder/) 41 | and [Hugging Face documentation](https://huggingface.co/docs/hub/spaces-sdks-docker) 42 | for more details. 43 | -------------------------------------------------------------------------------- /vignettes/how-to/set-application-run-parameters.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Set application run parameters" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Set application run parameters} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | When running a Shiny application you might want to set parameters like port or host. 11 | In Rhino, this can be done the same as in a regular Shiny application 12 | - by passing arguments to [`shiny::runApp`](https://shiny.posit.co/r/reference/shiny/1.7.4/runapp). 13 | 14 | For example, if you want to run your application with the host set to `0.0.0.0` 15 | (so you can serve it in your local network) and port set to `5000`, you can run: 16 | 17 | ```r 18 | # R console 19 | 20 | shiny::runApp(host = "0.0.0.0", port = 5000) 21 | ``` 22 | 23 | If you want to make your settings permanent, you can modify your `.Rprofile` 24 | file and set [Shiny options](https://shiny.posit.co/r/reference/shiny/1.7.4/shinyoptions) there: 25 | 26 | ```r 27 | # .Rprofile 28 | 29 | if (file.exists("renv")) { 30 | source("renv/activate.R") 31 | } else { 32 | # The `renv` directory is automatically skipped when deploying with rsconnect. 33 | message("No 'renv' directory found; renv won't be activated.") 34 | } 35 | 36 | # Allow absolute module imports (relative to the app root). 37 | options(box.path = getwd()) 38 | 39 | # Shiny options 40 | options(shiny.host = "0.0.0.0") 41 | options(shiny.port = 5000) 42 | ``` 43 | 44 | Make sure you don't remove entries related to `renv` and `box` as 45 | they are required for your Rhino application to work! 46 | -------------------------------------------------------------------------------- /vignettes/how-to/use-addins.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Rhino Addins" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Rhino Addins} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | To further streamline your development process, a collection of Addins has been designed to integrate with RStudio. These Addins provide convenient shortcuts and tools for common tasks. 11 | 12 | These Addins enhance the RStudio development workflow by providing quick access to essential tasks and enabling background execution, allowing for better transitions between coding and task management. 13 | 14 | This guide shows Addins available for Rhino. 15 | 16 | # Addins 17 | 18 | ![](images/rhino_addins.png) 19 | 20 | RStudio [Addins](https://rstudio.github.io/rstudioaddins/) provide a mechanism for executing R functions interactively from within the RStudio IDE either through keyboard shortcuts, or through the Addins menu. 21 | 22 | 23 | # Available Addins 24 | 25 | ## Create a new Rhino Module 26 | 27 | Jump start your module development by creating a new R script document with a Rhino module template. This Addin sets the foundation for your module structure, letting you dive straight into coding. 28 | 29 | ## Format R Code 30 | 31 | Uses the `{styler}` package to automatically format R script. This Addin ensures consistency and readability. 32 | 33 | ## Lint R Code 34 | 35 | Uses the `{lintr}` package to check all R sources for style errors. Identify and address potential issues in your R scripts with ease. 36 | 37 | ## Run R Tests 38 | 39 | Uses the `{testhat}` package to run all unit tests in `tests/testthat` directory. Maintain your functions and components reliability. 40 | 41 | ## Build JavaScript 42 | 43 | Simplify the process of building JavaScript files using Babel and Webpack. Builds the `app/js/index.js` file into `app/static/js/app.min.js`. Choose to watch for changes, automating the build process whenever you save the JavaScript file. 44 | 45 | ## Build Sass Styles 46 | 47 | Effortlessly build Sass styles using Dart Sass or the `{sass}` R package. It builds the `app/styles/main.scss` file into `app/static/css/app.min.css`. Opt to watch for changes, allowing for automatic rebuilding of style sheets. 48 | 49 | ## Lint JavaScript 50 | 51 | Runs `ESLint` on the JavaScript sources in the `app/js` directory. It performs linting on JavaScript files with ease. Opt to fix issues automatically for fixing it directly. 52 | 53 | ## Lint Sass Styles 54 | 55 | Runs `Stylelint` on the Sass sources in the `app/styles` directory. Choose to automatically fix issues to streamline the process of linting Sass styles. 56 | 57 | ## Run End-to-End Tests 58 | 59 | Execute Cypress end-to-end tests for your application. Choose between interactive and non-interactive modes to validate application behavior. 60 | -------------------------------------------------------------------------------- /vignettes/how-to/use-bslib.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Use bslib with Rhino" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Use bslib with Rhino} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | # Introduction 11 | 12 | The `bslib` R package is a UI toolkit based on Bootstrap. 13 | Shiny uses it under the hood for its default appearance, 14 | but it's also possible to use `bslib` directly to gain access to: 15 | 16 | 1. Additional components (e.g. value boxes, accordions, tooltips). 17 | 2. Customization and theming. 18 | 3. A refined interface (e.g. `bslib::page_sidebar` instead of `shiny::sidebarLayout`). 19 | 4. The latest Bootstrap (v5). 20 | 21 | Rhino comes with built-in support for writing custom Sass code in `app/styles/main.scss`. 22 | This guide explains how to combine `bslib` and custom Sass code, allowing you to: 23 | 24 | 1. Share variables (e.g. colors and fonts) between `bslib` and custom Sass. 25 | 2. Use Bootstrap variables, functions and mixins in your custom Sass. 26 | 3. Further customize Bootstrap, e.g. override its mixins. 27 | 28 |

29 | If you don't want to write any custom Sass, 30 | you can use `bslib` as you would normally without any additional setup. 31 |

32 | 33 | # Steps 34 | 35 | 1. Add `bslib` to project dependencies: `rhino::pkg_install("bslib")`. 36 | 2. Use `sass: custom` configuration option in `rhino.yml`. 37 | 3. Bundle Rhino Sass with `bslib` theme, e.g.: 38 | 39 | ```r 40 | theme <- bslib$bs_theme(primary = "purple") |> 41 | bslib$bs_add_rules(sass$sass_file("app/styles/main.scss")) 42 | ``` 43 | 44 | You can create the `theme` object in `app/main.R` or in a dedicated file, e.g. `app/view/theme.R`. 45 | You need to define your UI using one of `bslib::page_*` layout functions, 46 | and pass the `theme` object as argument, e.g.: 47 | 48 | ```r 49 | #' @export 50 | ui <- function(id) { 51 | ns <- NS(id) 52 | bslib$page_fillable( 53 | theme = theme, 54 | shiny$h1("Hello!") 55 | ) 56 | } 57 | ``` 58 | 59 | You don't need to run `rhino::build_sass()`. 60 | Shiny will build it automatically when needed. 61 | 62 | With this setup you can use the `main.scss` file as you would normally, 63 | but with full access to Bootstrap and variables defined in `bs_theme()`, e.g.: 64 | 65 | ```scss 66 | h1 { 67 | color: tint-color($primary, 20%); 68 | } 69 | ``` 70 | 71 | ## Advanced use cases 72 | 73 | For advanced use cases, consider creating a complete [Sass layer](https://rstudio.github.io/sass/reference/sass_layer.html): 74 | 75 | ```r 76 | theme <- bslib$bs_bundle( 77 | bslib$bs_theme(), 78 | sass$sass_layer( 79 | functions = sass$sass_file("app/styles/functions.scss"), 80 | defaults = sass$sass_file("app/styles/defaults.scss"), 81 | mixins = sass$sass_file("app/styles/mixins.scss"), 82 | rules = sass$sass_file("app/styles/rules.scss") 83 | ) 84 | ) 85 | ``` 86 | -------------------------------------------------------------------------------- /vignettes/how-to/use-destructure-operator.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Use destructure operator" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Use destructure operator} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | > **Note:** The destructure operator is currently an experimental feature and its behavior may change in future releases. 18 | 19 | ## Overview 20 | 21 | The destructure operator `%<-%` allows you to extract multiple named values from a list into individual variables in a single assignment. 22 | While it was originally introduced to improve the ergonomics of working with Shiny modules, 23 | its clean syntax makes it valuable for any situation where you need to unpack multiple values from a named list. 24 | 25 | ## Basic Usage 26 | 27 | The operator works with any named list in R. The basic syntax is: 28 | 29 | ```r 30 | c(var1, var2) %<-% list_object 31 | ``` 32 | 33 | Here are the key features: 34 | 35 | 1. **Named List Requirements** 36 | - The right-hand side must be a named list 37 | - Data frames are not supported 38 | - Unnamed lists will raise an error 39 | 40 | 2. **Partial Extraction** 41 | - You can extract only the values you need 42 | - Order doesn't matter - matching is done by name 43 | 44 | ```r 45 | # Create a list with named elements 46 | config <- list(host = "localhost", port = 8080, debug = TRUE) 47 | 48 | # Extract only what you need 49 | c(host, port) %<-% config 50 | ``` 51 | 52 | 3. **Pipe Operator Compatibility** 53 | - Works with the native pipe operator (`|>`) 54 | - Requires wrapping the piped expression in parentheses 55 | 56 | ```r 57 | c(value) %<-% ( 58 | 123 |> 59 | list(value = _) 60 | ) 61 | ``` 62 | 63 | ## Shiny Modules Integration 64 | 65 | The destructure operator particularly shines when working with Shiny modules. Here's a practical example: 66 | 67 | ```r 68 | # app/view/module.R 69 | 70 | box::use( 71 | shiny[div, moduleServer, NS, numericInput, reactive], 72 | ) 73 | 74 | #' @export 75 | ui <- function(id) { 76 | ns <- NS(id) 77 | div( 78 | numericInput(ns("number"), "Enter a number", value = 0), 79 | numericInput(ns("number2"), "Enter another number", value = 0), 80 | ) 81 | } 82 | 83 | #' @export 84 | server <- function(id) { 85 | moduleServer(id, function(input, output, session) { 86 | return(list( 87 | number = reactive(input$number), 88 | number2 = reactive(input$number2) 89 | )) 90 | }) 91 | } 92 | ``` 93 | 94 | ```r 95 | # app/main.R 96 | 97 | box::use( 98 | rhino[`%<-%`], 99 | shiny[div, moduleServer, NS, renderText, textOutput], 100 | ) 101 | 102 | box::use( 103 | app/view/module, 104 | ) 105 | 106 | #' @export 107 | ui <- function(id) { 108 | ns <- NS(id) 109 | div( 110 | module$ui(ns("module")), 111 | textOutput(ns("result")) 112 | ) 113 | } 114 | 115 | #' @export 116 | server <- function(id) { 117 | moduleServer(id, function(input, output, session) { 118 | # Clean extraction of multiple reactive values 119 | c(number, number2) %<-% module$server("module") 120 | 121 | output$result <- renderText({ 122 | paste0("Sum of ", number(), " and ", number2(), " is ", number() + number2()) 123 | }) 124 | }) 125 | } 126 | ``` 127 | 128 | In this example, the module's server function returns a list of reactive values. 129 | The destructure operator provides a clean and intuitive way to extract these values into separate variables, making the code more readable and maintainable. 130 | -------------------------------------------------------------------------------- /vignettes/how-to/use-global-variables.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Use global variables" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Use global variables} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Rhino uses [`box`](https://appsilon.github.io/rhino/articles/explanation/box-modules.html) to ensure reusable and modular code, both from using packages and using the scripts within the app itself. Rhino favors the explicit over the implicit, and a local scope over a global scope. 11 | 12 | Sometimes we need global definitions however and storing constants is the most common reason for that. 13 | The suggested way to create global constants in Rhino is to define and export the constant inside its own `R` script in `app/logic`. 14 | 15 | ```r 16 | # app/logic/constant.R 17 | #' @export 18 | answer <- 42 19 | ``` 20 | 21 | The global constant can be imported and used in other modules. 22 | 23 | ```r 24 | # app/logic/another_module.R 25 | box::use(app/logic/constant[answer]) 26 | ``` 27 | 28 | Most of the time, it is sufficient to define a constant and then export and use it throughout the app. 29 | 30 | However, there are instances when an object may need to change its value as the app runs. In such cases, Rhino suggests the following ways to handle global variables. 31 | 32 | Note: Using global variables can make the app more difficult to manage and understand. 33 | If the variable can change its value at any point in the app, future changes to the app must consider how this global variable will be affected. A changing global variable may also make testing difficult because tests may affect the global variables. The tests are no longer independent of each other. 34 | 35 | # Global Variables 36 | 37 | ## Vanilla R 38 | 39 | In `R`, [global variables live inside `.GlobalEnv`](http://adv-r.had.co.nz/Environments.html). Global variables can be updated within a function using `<<-`. 40 | 41 | ```r 42 | # constants.R 43 | answer <- 42 44 | set_answer <- function(new_answer) { 45 | answer <<- new_answer 46 | } 47 | ``` 48 | 49 | ```r 50 | # main.R 51 | source("constants.R") 52 | 53 | print(answer) # 42 54 | set_answer(0) 55 | print(answer) # 0 56 | ``` 57 | 58 | When code is loaded with `box::use()`, global variables live inside the [module's own immutable environment](https://klmr.me/box/articles/mod-env-hierarchy.html#environments-1). Updating global variables with `<<-` will not work. 59 | 60 | ```r 61 | # app/logic/constants.R 62 | 63 | #' @export 64 | answer <- 42 65 | 66 | #' @export 67 | set_answer <- function(new_answer) { 68 | answer <<- new_answer 69 | } 70 | ``` 71 | 72 | ```r 73 | # app/main.R 74 | box::use(app/logic/constants) 75 | 76 | print(constants$answer) # 42 77 | constants$set_answer(0) # Error: cannot change value of locked binding. 78 | ``` 79 | 80 | ## Variables in a new environment 81 | 82 | To overcome `box`'s feature of limiting scope, Rhino suggests creating a new environment and use that environment to contain the global variables. 83 | 84 | ```r 85 | # app/logic/__init__.R 86 | 87 | #' @export 88 | global <- new.env() 89 | global$answer <- 42 90 | 91 | #' @export 92 | set_answer <- function(new_answer) { 93 | global$answer <- new_answer 94 | } 95 | ``` 96 | 97 | ```r 98 | # app/logic/get_answer.R 99 | box::use( 100 | app/logic[global, set_answer], 101 | ) 102 | 103 | print(global$answer) # 42 104 | set_answer(0) 105 | print(global$answer) # 0 106 | ``` 107 | 108 | ## Variables in `.GlobalEnv` 109 | 110 | Alternatively, variables can still be stored in and imported from `.GlobalEnv`. The variable must also be defined and updated using `<-`. 111 | 112 | ```r 113 | # app/logic/__init__.R 114 | .GlobalEnv$answer <- 42 115 | 116 | #' @export 117 | set_answer <- function(new_answer) { 118 | .GlobalEnv$answer <- new_answer 119 | } 120 | ``` 121 | 122 | ```r 123 | # app/logic/get_answer.R 124 | box::use(app/logic[set_answer]) 125 | 126 | print(.GlobalEnv$answer) # 42 127 | set_answer(0) 128 | print(.GlobalEnv$answer) # 0 129 | ``` 130 | 131 | # Session Variables 132 | 133 | Rhino suggests using arguments in module servers for explicit handling of session variables or user inputs. 134 | 135 | ```r 136 | module_ui <- function(id) { 137 | ns <- NS(id) 138 | textOutput(ns("answer")) 139 | } 140 | 141 | module_server <- function(id, answer) { 142 | moduleServer(id, function(input, output, session) { 143 | output$answer <- renderText(answer()) 144 | }) 145 | } 146 | 147 | shinyApp( 148 | ui = bootstrapPage( 149 | textInput("answer", "Answer"), 150 | module_ui("module") 151 | ), 152 | server = function(input, output, session) { 153 | answer <- reactive(input$answer) 154 | module_server("module", answer) 155 | } 156 | ) 157 | ``` 158 | 159 | However, `shiny` has support for [`session$userData`](https://shiny.rstudio.com/reference/shiny/latest/session.html), an environment that can store session-specific data. 160 | 161 | ```r 162 | module_ui <- function(id) { 163 | ns <- NS(id) 164 | textOutput(ns("answer")) 165 | } 166 | 167 | module_server <- function(id) { 168 | moduleServer(id, function(input, output, session) { 169 | output$answer <- renderText(session$userData$answer()) 170 | }) 171 | } 172 | 173 | shinyApp( 174 | ui = bootstrapPage( 175 | textInput("answer", "Answer"), 176 | module_ui("module") 177 | ), 178 | server = function(input, output, session) { 179 | session$userData$answer <- reactive(input$answer) 180 | module_server("module") 181 | } 182 | ) 183 | ``` 184 | 185 | All modules have access to the variables inside `session$userData`. 186 | -------------------------------------------------------------------------------- /vignettes/how-to/use-polished.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Use polished" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Use polished} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Rhino puts a strong emphasis on modularization 11 | and for consistency, even the outermost UI and server are defined as a Shiny module. 12 | Unfortunately, this approach is not compatible with the authentication mechanism implemented 13 | in [`polished` package](https://github.com/Tychobra/polished). 14 | 15 | 16 | To overcome this you can setup a [legacy entrypoint](https://appsilon.github.io/rhino/reference/app.html#legacy-entrypoint) 17 | in your `rhino.yml`. 18 | Please be aware that it is a workaround and not a setting recommended for all cases: 19 | 20 | ```yml 21 | legacy_entrypoint: box_top_level 22 | ``` 23 | 24 | After adding `polished` to your [dependencies](https://appsilon.github.io/rhino/articles/how-to/manage-r-dependencies.html) 25 | you can use it in `app/main.R` as follows: 26 | 27 | ```r 28 | box::use( 29 | polished, 30 | shiny, 31 | ) 32 | 33 | polished$polished_config( 34 | app_name = "rhino_app", # the name of your application 35 | api_key = Sys.getenv("API_KEY") # API key obtained from polished.tech 36 | ) 37 | 38 | #' @export 39 | ui <- polished$secure_ui( 40 | shiny$fluidPage( 41 | shiny$fluidRow( 42 | shiny$column( 43 | 6, 44 | shiny$h1("Hello Shiny!") 45 | ), 46 | shiny$column( 47 | 6, 48 | shiny$br(), 49 | shiny$actionButton( 50 | "sign_out", 51 | "Sign Out", 52 | icon = shiny$icon("sign-out-alt"), 53 | class = "pull-right" 54 | ) 55 | ), 56 | shiny$column( 57 | 12, 58 | shiny$verbatimTextOutput("user_out") 59 | ) 60 | ) 61 | ) 62 | ) 63 | 64 | 65 | #' @export 66 | server <- polished$secure_server( 67 | function(input, output, session) { 68 | output$user_out <- shiny$renderPrint({ 69 | session$userData$user() 70 | }) 71 | 72 | shiny$observeEvent(input$sign_out, { 73 | polished$sign_out_from_shiny() 74 | session$reload() 75 | }) 76 | } 77 | ) 78 | 79 | 80 | ``` 81 | 82 | The guide on how to configure Polished Authentication with your Shiny app can be found [here](https://polished.tech/docs/01-get-started). 83 | -------------------------------------------------------------------------------- /vignettes/how-to/use-shiny-fluent.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Use shiny.fluent with Rhino" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Use shiny.fluent with Rhino} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | [`shiny.fluent` package](https://appsilon.github.io/shiny.fluent/) 11 | brings the beautiful look of Microsoft Fluent UI to the world of Shiny. 12 | You can use it to build your Rhino applications - it works out of the box. 13 | 14 | Check out [this tutorial](https://appsilon.github.io/shiny.fluent/articles/st-shiny-fluent-and-rhino.html) 15 | to learn how you can combine those two packages. 16 | -------------------------------------------------------------------------------- /vignettes/how-to/use-shinymanager.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Use shinymanager" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Use shinymanager} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | Rhino puts strong emphasis on modularization 11 | and for consistency even the outermost UI and server are defined as a Shiny module. 12 | Unfortunately, [`shinymanager::secure_app()`](https://datastorm-open.github.io/shinymanager/reference/secure-app.html) 13 | cannot be placed in a Shiny module as it is designed to be passed directly to `shiny::shinyApp()`. 14 | 15 | To overcome this you can setup a [legacy entrypoint](https://appsilon.github.io/rhino/reference/app.html#legacy-entrypoint) 16 | in your `rhino.yml`. 17 | Please be aware that it is a workaround and not a setting recommended for all cases: 18 | 19 | ```yml 20 | legacy_entrypoint: box_top_level 21 | ``` 22 | 23 | After adding `shinymanager` to your [dependencies](https://appsilon.github.io/rhino/articles/how-to/manage-r-dependencies.html) 24 | you can use it in `app/main.R` as follows: 25 | 26 | ```r 27 | box::use( 28 | shiny, 29 | shinymanager, 30 | ) 31 | 32 | # Define your `check_credentials` function. 33 | # This is just an example. Do not hard-code the credentials in your actual application. 34 | check_credentials <- shinymanager$check_credentials( 35 | data.frame(user = "admin", password = "admin") 36 | ) 37 | 38 | #' @export 39 | ui <- shinymanager$secure_app( # Wrap your entire UI in `secure_app()`. 40 | shiny$bootstrapPage( 41 | shiny$textInput("name", "Name"), 42 | shiny$textOutput("message") 43 | ) 44 | ) 45 | 46 | #' @export 47 | server <- function(input, output) { 48 | # Call `secure_server()` at the beginning of your server function. 49 | shinymanager$secure_server(check_credentials) 50 | output$message <- shiny::renderText(paste0("Hello ", input$name, "!")) 51 | } 52 | ``` 53 | 54 | This is just an example. 55 | **Do not hard-code the credentials** in your actual application. 56 | Store them in a database or use 57 | [environment variables](https://appsilon.github.io/rhino/articles/how-to/manage-secrets-and-environments.html). 58 | 59 | ### Bookmarking 60 | 61 | If you want to use [bookmarking](https://shiny.posit.co/r/articles/share/bookmarking-state/) 62 | together with `shinymanager`, 63 | you will need to wrap the UI passed to `secure_app()` in a function: 64 | 65 | ```r 66 | shiny$enableBookmarking() 67 | 68 | #' @export 69 | ui <- shinymanager$secure_app( 70 | # Wrap the UI passed to `secure_app()` in a function with a `request` parameter. 71 | function(request) { 72 | shiny$bootstrapPage( 73 | shiny$bookmarkButton(), 74 | shiny$textInput("name", "Name"), 75 | shiny$textOutput("message") 76 | ) 77 | } 78 | ) 79 | ``` 80 | -------------------------------------------------------------------------------- /vignettes/how-to/use-shinytest2.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Use shinytest2" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Use shinytest2} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | If you have Node.js available on your machine, 11 | you can write end-to-end tests using [Cypress](https://www.cypress.io/) 12 | and run them with `rhino::test_e2e()` without any additional setup. 13 | If you'd prefer to use the `shinytest2` package instead, you will need to: 14 | 15 | 1. Run `rhino::pkg_install(c("shinytest2", "shinyvalidate"))`. 16 | 2. Create a test with `shinytest2::record_test()` or `shinytest2::use_shinytest2_test()` as usual. 17 | 3. Optionally you can remove the following files created by `shinytest2`, 18 | which are unnecessary in Rhino: 19 | 1. Runner (`tests/testthat.R`), 20 | used in R packages and executed by `R CMD check`. 21 | 2. Setup (`tests/testthat/setup-shinytest2.R`), 22 | used in traditional Shiny applications with `global.R` and sources in the `R` directory. 23 | 24 | The tests created by `shinytest2` are treated as any other `testthat` tests 25 | and can be run with `rhino::test_r()`. 26 | -------------------------------------------------------------------------------- /vignettes/how-to/use-static-files.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Use static files" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Use static files} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 |

11 | This article is a stub. We are working to edit and expand it. 12 |

13 | 14 | All files which should be available on the browser 15 | and are not supposed to change (so called "static" files) 16 | should go to the `app/static` directory. 17 | To include it in your app use e.g. `img(src = "static/images/appsilon-logo.png")`. 18 | Note the `static/` prefix in the `src` attribute. 19 | -------------------------------------------------------------------------------- /vignettes/how-to/write-javascript-code.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Write JavaScript code" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Write JavaScript code} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 |

11 | This article is a stub. We are working to edit and expand it. 12 |

13 | 14 | The `app/js/index.js` is the entrypoint for your JavaScript code. 15 | 16 | To use functions defined in this file in R you must export them: 17 | ```js 18 | export function sayHello() { console.log('Hello!'); } 19 | ``` 20 | 21 | and use an `App.` prefix when referring to them in your R code. 22 | ```r 23 | tags$button(onclick = "App.sayHello()") 24 | ``` 25 | -------------------------------------------------------------------------------- /vignettes/how-to/write-r-code.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How-to: Write R code" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{How-to: Write R code} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 |

11 | This article is a stub. We are working to edit and expand it. 12 |

13 | 14 | The entrypoint to your application is the `app/main.R` file. 15 | It can load packages and other files with `box::use()`. 16 | You should not use `::`, `library()` and `source()` calls in your code. 17 | 18 | Each file should have two `box::use()` statements at the top 19 | (one for packages and one for modules). 20 | Avoid `...`. Sort entries alphabetically. 21 | 22 | Each file in `app/view` should be a box + Shiny module. 23 | See `app/main.R` for example. 24 | -------------------------------------------------------------------------------- /vignettes/tutorial/images/basic_e2e_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/basic_e2e_test.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/chart.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/chart_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/chart_2.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/clicks_e2e_test_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/clicks_e2e_test_1.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/clicks_e2e_test_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/clicks_e2e_test_2.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/cypress_test_app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/cypress_test_app.gif -------------------------------------------------------------------------------- /vignettes/tutorial/images/e2e_in_CI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/e2e_in_CI.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/failing_e2e_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/failing_e2e_test.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/first_module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/first_module.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/init_application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/init_application.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/interactive_e2e_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/interactive_e2e_test.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/js_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/js_1.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/js_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/js_2.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/message_e2e_test_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/message_e2e_test_1.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/message_e2e_test_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/message_e2e_test_2.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/rstudio_wizard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/rstudio_wizard.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/styles_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/styles_1.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/styles_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/styles_2.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/table_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/table_1.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/table_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/table_2.png -------------------------------------------------------------------------------- /vignettes/tutorial/images/table_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Appsilon/rhino/047455698ba04794f1f97cc4380c1902fe77e97f/vignettes/tutorial/images/table_3.png --------------------------------------------------------------------------------