}}
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 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-experimental.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-stable.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-superseded.svg:
--------------------------------------------------------------------------------
1 |
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{
}}
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{
}}
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{
}}
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 | 
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
--------------------------------------------------------------------------------