Skip to content

Commit

Permalink
Merge pull request #559 from Appsilon/autoreload
Browse files Browse the repository at this point in the history
Add support for `shiny.autoreload`
  • Loading branch information
kamilzyla authored Feb 20, 2024
2 parents a5c65c0 + 976b8b9 commit db8e2f8
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 6 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: rhino
Title: A Framework for Enterprise Shiny Applications
Version: 1.6.0.9004
Version: 1.6.0.9005
Authors@R:
c(
person("Kamil", "Żyła", role = c("aut", "cre"), email = "[email protected]"),
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
when using a `legacy_entrypoint` ([#395](https://github.com/Appsilon/rhino/issues/395)).
* Force evaluation of arguments in higher-order functions
to avoid unexpected behavior due to lazy evaluation (internal).
3. Add support for [`shiny.autoreload`](https://shiny.posit.co/r/reference/shiny/latest/shinyoptions).

# [rhino 1.6.0](https://github.com/Appsilon/rhino/releases/tag/v1.6.0)

Expand Down
79 changes: 74 additions & 5 deletions R/app.R
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,15 @@ configure_logger <- function() {
}

load_main <- function(use_source, expect_shiny_module) {
if (use_source) {
main <- load_main_source()
} else {
main <- load_main_box()
loader <- function() {
if (use_source) {
main <- load_main_source()
} else {
main <- load_main_box()
}
normalize_main(main, expect_shiny_module)
}
normalize_main(main, expect_shiny_module)
load_main_with_autoreload(loader)
}

load_main_source <- function() {
Expand Down Expand Up @@ -161,6 +164,72 @@ normalize_server <- function(server, is_module = FALSE) {
}
}

load_main_with_autoreload <- function(loader) {
# There are two key components to make `shiny.autoreload` work:
# 1. When app files are modified, the autoreload callback updates the `main` module in `app_env`.
# 2. UI and server are functions which retrieve `main` from `app_env` each time they are called.
#
# We use the same method both for loading the main module initially and for reloading it.
# This guarantees consistent behavior regardless of whether user enables `shiny.autoreload`,
# or calls `shiny::runApp()` each time they want to see changes.
#
# We always register an autoreload callback.
# Shiny just won't call it unless `shiny.autoreload` option is set.

app_env <- new.env(parent = emptyenv())
load_main <- function() {
app_env$main <- loader()
}

load_main()
register_autoreload_callback(load_main)

list(
ui = function(request) {
app_env$main$ui(request)
},
server = function(input, output, session) {
app_env$main$server(input, output, session)
}
)
}

register_autoreload_callback <- function(callback) {
# The autoreload callbacks are not in the public API of Shiny,
# so we need to be extra careful when using them.
callbacks <- get0("autoReloadCallbacks", envir = loadNamespace("shiny"))
if (is.null(callbacks)) {
cli::cli_alert_warning("Skipping autoreload setup - this version of Shiny doesn't support it.")
return()
}

force(callback) # Avoid the pitfalls of lazy evaluation.
safe_callback <- function() {
warn_on_error({
callback()
}, "Rhino couldn't reload the main module")
}

warn_on_error({
autoreload_callback$clear()
autoreload_callback$clear <- callbacks$register(safe_callback)
}, "Unexpected error while registering an autoreload callback")
}

autoreload_callback <- new.env(parent = emptyenv())
autoreload_callback$clear <- function() NULL

warn_on_error <- function(expr, text) {
tryCatch(
expr,
error = function(condition) {
cli::cli_alert_warning(paste0(
text, ": ", conditionMessage(condition)
))
}
)
}

make_app <- function(main) {
shiny::shinyApp(
ui = with_head_tags(main$ui),
Expand Down
9 changes: 9 additions & 0 deletions tests/testthat/test-app.R
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ describe("normalize_server()", {
})
})

describe("warn_on_error()", {
it("catches an error and prints it with an appended message", {
expect_message(
warn_on_error(stop("some_error"), "some_message"),
"some_message: some_error"
)
})
})

describe("with_head_tags()", {
it("attaches a head tag to UI", {
ui <- function(request) shiny::tags$div("test")
Expand Down
9 changes: 9 additions & 0 deletions vignettes/faq.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ You can run a Rhino application exactly the same as a regular Shiny app:

</details>

<details>
<summary>How to automatically reload the application during development?</summary>

Call `options(shiny.autoreload = TRUE)` in your R session.
Shiny will monitor the app directory and reload all connected sessions if any changes are detected.
More details can be found in [Shiny reference](https://shiny.posit.co/r/reference/shiny/latest/shinyoptions).

</details>

<details>
<summary>How to use a specific port when running a Rhino application?</summary>

Expand Down

0 comments on commit db8e2f8

Please sign in to comment.