-
Notifications
You must be signed in to change notification settings - Fork 16
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 R Shiny with webR #51
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this looks great! I had a couple small questions/comments but I'm fine with it if you want to get this merged and address those things later.
@@ -458,7 +466,7 @@ export default function Editor({ | |||
// =========================================================================== | |||
// Buttons | |||
// =========================================================================== | |||
const formatCodeButton = ( | |||
const formatCodeButton = appEngine === 'python' ? ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if we could use the styler package to reformat R code, but we can save that for a later PR. (It's probably also a good idea to see how heavy the dependencies are for styler.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, we should definitely look into integrating something like styler. For the moment the idea was mostly just so that a broken format button does not appear when using the R engine.
I think it makes sense to experiment with this in a later PR, as this one is already pretty large.
@@ -400,10 +402,15 @@ let channelListenerRegistered = false; | |||
function ensureOpenChannelListener(pyodideProxy: PyodideProxy): void { | |||
if (channelListenerRegistered) return; | |||
|
|||
window.addEventListener("message", (event) => { | |||
window.addEventListener("message", async (event) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure it's safe to make this an async function? I'm not saying that I think it's not safe -- I just want to make sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like pyodideProxy.openChannel()
is itself async
, so even without this change I don’t think there’s any guarantee that the channel will be opened by the time the event handler returns.
Add COOP & COEP headers to esbuild dev server for cross-origin isolation. This enables use of SharedArrayBuffer for Wasm. Add CORP header to esbuild dev server to permit embedding shinylive into an outer window. Use the shinylive service worker to add CORP/COOP/COEP headers to responses. Allows shinylive to work with webR when running in environments where it is not possible to change the HTTP headers, e.g. under GitHub pages.
A new argument `engine` is added to runApp() that selects webR or Pyodide as the underlying engine to use. The main app selects a React hook `useWebR()` or `usePyodide()` based on this argument. The proxy handle variable `proxyHandle` is typed as a union, `ProxyHandle = PyodideProxyHandle | WebRProxyHandle`, and the property `engine` is added as a way to discriminate between which Wasm engine is currently loaded. Hooks relying on Pyodide are disabled through early return when the `proxyHandle.engine` property is not equal to "pyodide".
* Update Terminal component to use a general `proxyHandle`. * Tweak the `runCode` Ref to return the next prompt as a string when running with the webR engine. This allows for the debuging `Browse[1]>` and input `readline()` prompts when using R. * Maintain default ">>> " prompt when running under Pyodide.
The `useEffect()` hook setting up the `runApp()` and `stopApp()` methods is tweaked so that the current hook runs only when the Wasm engine is Pyodide. A second `useEffect()` hook is added for setting up the methods to start an R shiny server with webR.
Three Makefile targets are added to build and serve with webR as the default engine: * make serve-r * make serve-prod-r * make buildjs-prod-r
With this commit the Service Worker will only inject the HTTP headers required for Cross-Origin Isolation if the URL or referrer URL contains the parameter `coi=1`. Now, Shinylive for Python is not served with COI headers injected by default. This fixes the Map example when running under Pyodide. If an app using the webR engine is run, and the page is not Cross-Origin Isolated, the parameter `coi=1` is added to the current location URL and the page refreshed. With this, using webR without the required COI headers from the server is handled automatically.
Minor update: I've rebased on So, the included R examples are now just the basic ones that ship with the R shiny package. We can revisit adding more complex R shiny examples after this PR has been merged. |
With this, cross-origin requests for embedded content are sent without credentials and the responses are allowed without an explicit permission via the CORP header in the response. This fixes embedding certain types of content in webR Shinylive apps, while still allowing the option of using cross-origin isolation and SharedArrayBuffer with webR. Fixes posit-dev#49, posit-dev#51.
This is a fairly large PR with several moving parts. My apologies that it is not broken up into more manageable pieces.
This set of commits extends Shinylive's infrastructure to include running R Shiny apps using webR. The addition of the webR Wasm engine is based heavily on the Pyodide implementation, to the point of some duplication. In the future we might want to abstract shared pieces out, but hopefully this is OK for now.
The new code for loading webR into the page, executing R code, and setting things up for running Shiny is contained in the files
hooks/useWebR.tsx
andwebr-proxy.ts
.New (but similar to existing) functions have been added to
messageporthttp.ts
andmessageportwebsocket-channel.ts
for handling traffic for R Shiny apps separately to Python Shiny apps.When webR loads a Shiny app, a httpuv app object is created and stored in a global registry. When HTTP requests are received from the service worker, they are converted into rook-style objects and the correct
abbObj$call()
is invoked directly for a response. When a WebSocket connection is initialised, traffic is handled via callbacks from R. Proxy R object references to the callback functions are stored and used to send WS traffic from the client to the Shiny server.Going the other way, WS traffic from the server to the client is first serialised then sent via a standard webR output channel to be handled by Shinylive. A map of Shiny app names to specfic app's
toClient()
functions is maintained and then used to forward the WS output message content onto the correct Shiny client.When a Shiny client launches in an iframe and opens a WebSocket, an
openChannel
message is fired to the containing window. This happens for both Python and R and causes cross-talk if both engines are running at the same time. The handlers for these messages have been tweaked so that a communication channel is only opened with a specific Wasm engine if that Wasm engine recognises the app name in its.shiny_app_registry
(for webR) or_shiny_app_registry
for (Pyodide).The selection of which Wasm engine will be used is handled in
App.tsx
. TheApp
component has a new attributeappEngine
, of typeAppEngine = 'python' | 'r'
. The functionrunApp()
now also takes the Wasm engine as an optional argument. The chosen Wasm engine is loaded into the page by using the argument's value to select which of the engine's hooks are used.The new
appEngine
argument is optional and defaults to a defined constant inbuild.ts
. By default,build.ts
sets the defaultappEngine
to "python". The intention here is that current scripts and CI set up for building the Shinylive website and assets pack will hopefully be unaffected by these changes, by defaulting to using "python" as before.However, when
build.ts
is invoked with--r
as one of its arguments, the behavior is changed so that the default value for theappEngine
argument is'r'
instead. With this, a version of the Shinylive website that loads Shiny for R, running on webR, can be built and served at some other URL, perhaps something like shinylive.io/r/.In any case, for either type of build both R and/or Python are available to be used in a web page by invoking
runApp()
with eitherr
orpython
as theappEngine
argument. The same Shinylive asset pack and service worker script can be used to launch both engines. This will be useful later when loading multiple apps in the same page, such as with quarto-shinylive.Several make recipes have been added to
Makefile
:make webr
- Copy the webR distribution fromnode_modules
into the shinylive assets tree. This step has also been added tomake all
.make serve-r
,make serve-prod-r
- Build and serve a version of the Shinylive website for R Shiny.make buildjs-prod-r
- Build a version of the Shinylive website for R Shiny.These commands are useful for building versions of the Shinylive editor and examples website that defaults to using webR and loading R examples, rather than Python.
The previous
make serve
,make buildjs
etc. recipes have been left as-is. They should continue to build the Shinylive website with Python as the default engine, as before.The
examples
directory has been reorganised intopython
andr
subdirectories, and I have added some of the examples from https://github.com/rstudio/shiny-examples. The Shinylive website checks theappEngine
argument provided to the rootApp
component and filters out the examples that don't match.The
Editor
and other React components have been updated to detect Shiny for R apps, by extending the search to includeapp.R
andserver.R
.The "Format code" button is hidden when running under webR. Probably more work can be done later to add a full LSP server for R, running under webR. But right now, the editor just doesn't handle any of those extra features when running with the webR engine.
The
Terminal
component is extended to support webR. The component is tweaked a little so that the correct prompt string is shown (>
, or other prompts from R, rather than a fixed>>>
), and a Ctrl-C handler has been added so that running R code can be interrupted.WebR requires that the containing web page is Cross-Origin Isolated (COI) to work when running with Shinylive. This happens when the page is served with certain HTTP headers set. The following changes have been made to assist in loading a webR Shinylive app under COI:
The
build.ts
dev server has been tweaked to serve its content with the required headers, but only when the--r
argument is supplied.The shinylive service worker is extended so that it can inject the required HTTP headers if they are missing. The service worker has not been set up to do this by default, because it can break Shinylive for Python examples that rely on loading cross-origin assets into the page (e.g. the Map example). So, in this PR the service worker has been set up to only inject the COI headers for webR if it has been asked to, by setting the URL of the loading page (or the referrer URL) to contain the query parameter
"coi=1"
. Servers where files are already served with COI headers don't need to include the query.The
runApp()
function has been tweaked so that if theengine
argument is'r'
, and the page is not already COI, and the query parameter is not in the URL already, then thecoi=1
query is added to the page URL and the page refreshed. By adding this, if a webR Shinylive app is used in a web page (e.g. part of a quarto-shinylive document) and the page is not already COI, then the page will refresh after the first load and the service worker injects the required headers. This fixes running Shinylive apps in non-COI contexts, such as GitHub pages.Note that loading webR's Shiny takes a fair while longer than Pyodide's Shiny for Python. This is due to package downloading. We might still be able to improve this in time by reducing Shiny's prerequisite R package download sizes.
For this initial introduction, the standard webr npm package is bundled with Shinylive. However, in the future I'd like to instead use a different version of the package built specially for Shinylive that already includes all the R packages required to launch R Shiny on its virtual filesystem. That should improve startup time quite a lot.
I think that's pretty much all the major changes. Please let me know if you have any questions or see anything that looks broken or like it won't work.
Example Shinylive for R deployment: https://georgestagg.github.io/shinylive/examples/
Example Quarto doc with Shiny for R & Shiny for Python examples: https://georgestagg.github.io/quarto-shinylive-demo/