Skip to content

Commit

Permalink
feat: Make auto-reload work with source entrypoint
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilzyla committed Feb 2, 2024
1 parent c4eed32 commit e6728b4
Showing 1 changed file with 146 additions and 120 deletions.
266 changes: 146 additions & 120 deletions R/app.R
Original file line number Diff line number Diff line change
@@ -1,3 +1,80 @@
#' Rhino application
#'
#' The entrypoint for a Rhino application.
#' Your `app.R` should contain nothing but a call to `rhino::app()`.
#'
#' This function is a wrapper around `shiny::shinyApp()`.
#' It reads `rhino.yml` and performs some configuration steps (logger, static files, box modules).
#' You can run a Rhino application in typical fashion using `shiny::runApp()`.
#'
#' Rhino will load the `app/main.R` file as a box module (`box::use(app/main)`).
#' It should export two functions which take a single `id` argument -
#' the `ui` and `server` of your top-level Shiny module.
#'
#' # Legacy entrypoint
#'
#' It is possible to specify a different way to load your application
#' using the `legacy_entrypoint` option in `rhino.yml`:
#' 1. `app_dir`: Rhino will run the app using `shiny::shinyAppDir("app")`.
#' 2. `source`: Rhino will `source("app/main.R")`.
#' This file should define the top-level `ui` and `server` objects to be passed to `shinyApp()`.
#' 3. `box_top_level`: Rhino will load `app/main.R` as a box module (as it does by default),
#' but the exported `ui` and `server` objects will be considered as top-level.
#'
#' The `legacy_entrypoint` setting is useful when migrating an existing Shiny application to Rhino.
#' It is recommended to transform your application step by step:
#' 1. With `app_dir` you should be able to run your application right away
#' (just put the files in the `app` directory).
#' 2. With `source` setting your application structure must be brought closer to Rhino,
#' but you can still use `library()` and `source()` functions.
#' 3. With `box_top_level` you can be confident that the whole app is properly modularized,
#' as box modules can only load other box modules (`library()` and `source()` won't work).
#' 4. The last step is to remove the `legacy_entrypoint` setting completely.
#' Compared to `box_top_level` you'll need to make your top-level `ui` and `server`
#' into a [Shiny module](https://shiny.rstudio.com/articles/modules.html)
#' (functions taking a single `id` argument).
#'
#' @return An object representing the app (can be passed to `shiny::runApp()`).
#'
#' @examples
#' \dontrun{
#' # Your `app.R` should contain nothing but this single call:
#' rhino::app()
#' }
#' @export
app <- function() {
setup_box_path()
configure_logger()
shiny::addResourcePath("static", fs::path_wd("app", "static"))

entrypoint <- read_config()$legacy_entrypoint
if (identical(entrypoint, "app_dir")) {
return(shiny::shinyAppDir("app"))
}

if (identical(entrypoint, "source")) {
load_main <- load_main_source
} else if (identical(entrypoint, "box_top_level")) {
load_main <- load_main_box_top_level
} else if (is.null(entrypoint)) {
load_main <- load_main_box_shiny_module
} else {
stop()
}

app_env <- load_app(load_main)
ui <- function(request) {
app_env$main$ui(request)
}
server <- function(input, output, session) {
app_env$main$server(input, output, session)
}
shiny::shinyApp(
ui = with_head_tags(ui),
server = reparse(server)
)
}

setup_box_path <- function() {
# Normally `box.path` is set in `.Rprofile` and used for the whole R session,
# however `shinytest2` launches the application in a new process which doesn't source `.Rprofile`.
Expand Down Expand Up @@ -35,31 +112,87 @@ configure_logger <- function() {
}
}

reparse <- function(f) {
eval(parse(text = deparse(f)), envir = environment(f))
load_app <- function(load_main) {
app_env <- new.env(parent = emptyenv())
app_env$main <- load_main()
register_reload_callback(function() {
app_env$main <- load_main()
})
app_env
}

load_main_source <- function() {
main <- source_main()
list(
ui = normalize_ui(main$ui),
server = normalize_server(main$server)
)
}

load_main_box_top_level <- function() {
main <- box_use_main()
list(
ui = normalize_ui(main$ui),
server = normalize_server(main$server)
)
}

load_main_box_shiny_module <- function() {
main <- box_use_main()
list(
ui = normalize_ui_module(main$ui),
server = normalize_server_module(main$server)
)
}

load_app_source <- function() {
source_main <- function() {
main <- new.env(parent = globalenv())
source(fs::path("app", "main.R"), local = main)

main
}

load_app_box <- function() {
box_use_main <- function() {
# Silence "no visible binding" notes raised by `box::use()` on R CMD check.
app <- NULL
main <- NULL

app_env <- new.env(parent = baseenv())
load_main <- function() {
box::purge_cache()
local(box::use(app/main), app_env)
box::purge_cache()
box::use(app/main)

main
}

normalize_ui <- function(ui) {
if (!is.function(ui)) {
function(request) ui
} else if (length(formals(ui)) == 0) {
function(request) ui()
} else {
function(request) ui(request)
}
}

load_main()
register_reload_callback(load_main)
normalize_ui_module <- function(ui_module) {
function(request) ui_module("app")
}

app_env
normalize_server <- function(server) {
if ("session" %in% formalArgs(server)) {
function(input, output, session) {
server(input = input, output = output, session = session)
}
} else {
function(input, output, session) {
server(input = input, output = output)
}
}
}

normalize_server_module <- function(server_module) {
function(input, output, session) {
server_module("app")
}
}

reload_callback <- new.env(parent = emptyenv())
Expand Down Expand Up @@ -93,113 +226,6 @@ with_head_tags <- function(ui) {
}
}

normalize_ui <- function(ui) {
if (!is.function(ui)) {
function(request) ui
} else if (length(formals(ui)) == 0) {
function(request) ui()
} else {
function(request) ui(request)
}
}

normalize_server <- function(server) {
if ("session" %in% formalArgs(server)) {
function(input, output, session) {
server(input = input, output = output, session = session)
}
} else {
function(input, output, session) {
server(input = input, output = output)
}
}
}

#' Rhino application
#'
#' The entrypoint for a Rhino application.
#' Your `app.R` should contain nothing but a call to `rhino::app()`.
#'
#' This function is a wrapper around `shiny::shinyApp()`.
#' It reads `rhino.yml` and performs some configuration steps (logger, static files, box modules).
#' You can run a Rhino application in typical fashion using `shiny::runApp()`.
#'
#' Rhino will load the `app/main.R` file as a box module (`box::use(app/main)`).
#' It should export two functions which take a single `id` argument -
#' the `ui` and `server` of your top-level Shiny module.
#'
#' # Legacy entrypoint
#'
#' It is possible to specify a different way to load your application
#' using the `legacy_entrypoint` option in `rhino.yml`:
#' 1. `app_dir`: Rhino will run the app using `shiny::shinyAppDir("app")`.
#' 2. `source`: Rhino will `source("app/main.R")`.
#' This file should define the top-level `ui` and `server` objects to be passed to `shinyApp()`.
#' 3. `box_top_level`: Rhino will load `app/main.R` as a box module (as it does by default),
#' but the exported `ui` and `server` objects will be considered as top-level.
#'
#' The `legacy_entrypoint` setting is useful when migrating an existing Shiny application to Rhino.
#' It is recommended to transform your application step by step:
#' 1. With `app_dir` you should be able to run your application right away
#' (just put the files in the `app` directory).
#' 2. With `source` setting your application structure must be brought closer to Rhino,
#' but you can still use `library()` and `source()` functions.
#' 3. With `box_top_level` you can be confident that the whole app is properly modularized,
#' as box modules can only load other box modules (`library()` and `source()` won't work).
#' 4. The last step is to remove the `legacy_entrypoint` setting completely.
#' Compared to `box_top_level` you'll need to make your top-level `ui` and `server`
#' into a [Shiny module](https://shiny.rstudio.com/articles/modules.html)
#' (functions taking a single `id` argument).
#'
#' @return An object representing the app (can be passed to `shiny::runApp()`).
#'
#' @examples
#' \dontrun{
#' # Your `app.R` should contain nothing but this single call:
#' rhino::app()
#' }
#' @export
app <- function() {
setup_box_path()
configure_logger()
shiny::addResourcePath("static", fs::path_wd("app", "static"))

entrypoint <- read_config()$legacy_entrypoint
if (identical(entrypoint, "app_dir")) {
shiny::shinyAppDir("app")
} else if (identical(entrypoint, "source")) {
main <- load_app_source()
ui <- normalize_ui(main$ui)
server <- main$server
shiny::shinyApp(
ui = with_head_tags(ui),
server = main$server
)
} else if (identical(entrypoint, "box_top_level")) {
app_env <- load_app_box()
ui <- function(request) {
normalize_ui(app_env$main$ui)(request)
}
server <- function(input, output, session) {
normalize_server(app_env$main$server)(input, output, session)
}
shiny::shinyApp(
ui = with_head_tags(ui),
server = reparse(server)
)
} else if (is.null(entrypoint)) {
app_env <- load_app_box()
ui <- function(request) {
app_env$main$ui("app")
}
server <- function(input, output, session) {
app_env$main$server("app")
}
shiny::shinyApp(
ui = with_head_tags(ui),
server = reparse(server)
)
} else {
stop()
}
reparse <- function(f) {
eval(parse(text = deparse(f)), envir = environment(f))
}

0 comments on commit e6728b4

Please sign in to comment.