Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for shiny.autoreload #559

Merged
merged 4 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
))
}
)
}
kamilzyla marked this conversation as resolved.
Show resolved Hide resolved

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
Loading