-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathapp.R
More file actions
272 lines (246 loc) · 8.69 KB
/
app.R
File metadata and controls
272 lines (246 loc) · 8.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#' 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() {
entrypoint <- read_config()$legacy_entrypoint
configure_box()
configure_static()
configure_logger()
if (identical(entrypoint, "app_dir")) {
return(shiny::shinyAppDir("app"))
}
make_app(load_main(
use_source = identical(entrypoint, "source"),
expect_shiny_module = is.null(entrypoint)
))
}
configure_box <- 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`.
if (is.null(getOption("box.path"))) {
options(box.path = getwd())
}
}
configure_static <- function() {
shiny::addResourcePath("static", fs::path_wd("app", "static"))
}
configure_logger <- function() {
config <- config::get()
log_level <- config$rhino_log_level
log_file <- config$rhino_log_file
if (!is.null(log_level)) {
logger::log_threshold(log_level)
} else {
cli::cli_alert_warning(
"Skipping log level configuration, 'rhino_log_level' field not found in config."
)
}
if (!is.null(log_file)) {
if (!is.na(log_file)) {
# Use an absolute path to avoid the effects of changing the working directory when the app
# runs.
if (!fs::is_absolute_path(log_file)) {
log_file <- fs::path_wd(log_file)
}
logger::log_appender(logger::appender_file(log_file))
}
} else {
cli::cli_alert_warning(
"Skipping log file configuration, 'rhino_log_file' field not found in config."
)
}
}
load_main <- function(use_source, expect_shiny_module) {
loader <- function() {
if (use_source) {
main <- load_main_source()
} else {
main <- load_main_box()
}
main <- normalize_main(main, expect_shiny_module)
list(ui = with_head_tags(main$ui), server = main$server)
}
load_main_with_autoreload(loader)
}
load_main_source <- function() {
main <- new.env(parent = globalenv())
source(fs::path("app", "main.R"), local = main)
main
}
load_main_box <- function() {
# Silence "no visible binding" notes raised by `box::use()` on R CMD check.
app <- NULL
main <- NULL
box::purge_cache()
box::use(app/main)
main
}
normalize_main <- function(main, is_module = FALSE) {
list(
ui = normalize_ui(main$ui, is_module),
server = normalize_server(main$server, is_module)
)
}
normalize_ui <- function(ui, is_module = FALSE) {
force(ui) # Avoid the pitfalls of lazy evaluation.
if (is_module) {
function(request) ui("app")
} else 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, is_module = FALSE) {
force(server) # Avoid the pitfalls of lazy evaluation.
if (is_module) {
function(input, output, session) {
server("app")
}
} else if ("session" %in% names(formals(server))) {
function(input, output, session) {
server(input = input, output = output, session = session)
}
} else {
function(input, output, session) {
server(input = input, output = output)
}
}
}
with_head_tags <- function(ui) {
head <- shiny::tags$head(
react_support(), # Needs to go before `app.min.js`, which defines the React components.
if (fs::file_exists("app/static/js/app.min.js")) {
shiny::tags$script(src = "static/js/app.min.js")
},
if (fs::file_exists("app/static/css/app.min.css")) {
shiny::tags$link(rel = "stylesheet", href = "static/css/app.min.css", type = "text/css")
},
if (fs::file_exists("app/static/favicon.ico")) {
shiny::tags$link(rel = "icon", href = "static/favicon.ico", sizes = "any")
}
)
force(ui) # Avoid the pitfalls of lazy evaluation.
function(request) {
shiny::tagList(head, ui(request))
}
}
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 = main$ui,
server = fix_server(main$server)
)
}
# A workaround for issues with server reloading:
# https://github.com/Appsilon/rhino/issues/157
#
# For Shiny to reload the app correctly, the body of the server function must meet two criteria:
# 1. It must be wrapped in curly braces.
# 2. It must have source reference information attached, i.e. `srcref` attributes.
fix_server <- function(server) {
force(server) # Avoid the pitfalls of lazy evaluation.
eval(parse(
text = "function(input, output, session) { server(input, output, session) }",
keep.source = TRUE # Ensure `srcref` attributes are attached.
))
}