micronaut-views icon indicating copy to clipboard operation
micronaut-views copied to clipboard

Feature: Support for React JS server side rendering

Open mikehearn opened this issue 10 months ago • 7 comments

This work in progress PR adds support for rendering ReactJS views server side. It sports:

  • The GraalJS engine run in multithreaded mode for high performance. Threads share the JIT compiled code and can render in parallel.
  • Javascript can be sandboxed (with the next major version of GraalJS).
  • Transparent support for both React and Preact (a lighter weight version of the React runtime).
  • Customize the Javascript used to invoke SSR to add features like head managers, or use the prepackaged default scripts to get going straight away.
  • Full documentation with tutorial showing you how to prepare your Javascript.
  • You can pass any @Introspectable Java objects to use as props for your page components. This is convenient for passing in things like the user profile info.
  • ~You can integrate fetcher libraries like SWR so Micronaut Views React can pre-fetch the API response server-side and then pass it to the client in the initial HTML. The docs show how to integrate this with the useSWR hook, which is the recommended way to use this feature. This reduces latency and improves robustness.~
  • Logging from Javascript is sent to the Micronaut logs. console.log and related will go to the INFO level of the logger named js, console.error and Javascript exceptions will go to the ERROR level of the same.

To do list:

  • [x] Sandboxing (done but requires an unreleased GraalJS, is off by default as a consequence).
  • [x] Upgrade to Micronaut 4.4.0 and get rid of the thread hacks.
  • [x] Write more tests to show the sandbox actually works.
  • [ ] Load the user's JS bundle using the ResourceLoader API.
  • [ ] Try it out with a more complex React based app, instead of just hello world samples.
  • [ ] Work out if there's an easy way to attach a debugger to the server-side JS.
  • [x] Document that Micronaut Security will add props to your root object automatically.
  • [ ] Work with @sgammon to integrate his excellent work on JS libraries from Elide. This should fix the HTTP fetching implementation.

mikehearn avatar Apr 17 '24 12:04 mikehearn

CLA assistant check
All committers have signed the CLA.

CLAassistant avatar Apr 17 '24 12:04 CLAassistant

I've been talking with @sgammon about maybe using the code he's got in Elide for this PR. It would mean adding some new MIT licensed dependencies to the React Renderer module. In turn his code depends on the Kotlin standard library and GraalJS.

The benefits would be that his team has implemented some of the core Web/Node APIs on top of Micronaut already, for example the fetch API. The current PR provides a very MVP/prototype implementation of server side fetching. It's super basic, not parallelized at all, basically expects you to do overrides and hot patches to your code in order to use it. Implementing the fetch API and others would improve compatibility with existing JS, especially for larger projects. Doing this well though is a lot of work, so it makes sense to reuse Sam's effort.

I'll park this work for a week or two, to let Sam work out if/when he'd be able to do this effort. If we can just add a few deps and use them, I'd rather add it all into this one PR. If Sam won't have time, then I might take server-side fetch out of this PR and then look at adding it back in later after extracting the code myself, but that would depend on user demand.

mikehearn avatar Apr 26 '24 09:04 mikehearn

I should be able to review and get back to this within a day or two

sgammon avatar Apr 26 '24 18:04 sgammon

I've removed the half-baked server side fetch support for now, as that's the part that would change the most given Sam's work. The remaining tasks are small and can hopefully be finished soon. Then it should be ready to merge.

mikehearn avatar May 07 '24 16:05 mikehearn

@graemerocher I've done a few improvements in the last couple of commits, including to the docs and I also now expose the request URL to the Javascript (useful for page router components).

W.R.T. using ResourceLoader, what's the right way to do it with this API for:

  1. Reloading resources automatically if they change on disk, but without polling on every request.
  2. Taking into account viewsConfiguration.getFolder() as the base?

mikehearn avatar May 08 '24 14:05 mikehearn

You can probably use a scheduled job to check for changes and reload I imagine

graemerocher avatar May 08 '24 16:05 graemerocher

Actually I think we have a file watch API already. You need to configure the watch paths (see FileWatchConfiguration), then you can write a @EventListener for FileChangedEvent:

@EventListener
void filesChanged(FileChangedEvent event) {

}

graemerocher avatar May 08 '24 16:05 graemerocher

File watching support using the standard API is added, though using it requires disabling the build plugin auto-restart behavior of course, as well as configuring it via extra application.properties which the old solution didn't need.

mikehearn avatar May 16 '24 16:05 mikehearn

you could disable (or document how to disable) restart. See https://docs.micronaut.io/latest/api/io/micronaut/scheduling/io/watch/FileWatchConfiguration.html#setRestart(boolean)

graemerocher avatar May 17 '24 08:05 graemerocher

@graemerocher Done, thanks. Marked this PR as ready for review.

mikehearn avatar May 23 '24 12:05 mikehearn

CI failed due to:

Execution failed for task ':micronaut-views-react:findBaseline'.
> Could not find a previous version for 5.4.0

I guess that's expected?

mikehearn avatar May 23 '24 13:05 mikehearn

yes it is expected our builds have binary compatibility verification to ensure we don't break public API from the previous version. In this case this is a new module so there is no previous version so you need to add in the Gradle build of any new modules:

micronautBuild {
    binaryCompatibility.enabled.set(false)
}

graemerocher avatar May 23 '24 15:05 graemerocher

Now the CI failure is a bit harder to fix. I am assuming the very latest version of GraalVM, but it seems that the CI setup provides a GraalVM already which is older (17 instead of 21). Locally my tests pass, presumably because I'm using the newer GraalVM that unbundles the truffle languages. Changing the GraalVM build used by CI for the whole project might be a separate PR or project though.

mikehearn avatar May 23 '24 17:05 mikehearn

we have a 21 CI as well in the matrix, can you exclude the tests for 17?

graemerocher avatar May 24 '24 07:05 graemerocher

I just noticed a new problem. Some rebase has brought in a change where the blocking executor is now using virtual threads (loom) by default. Unfortunately Truffle is currently incompatible with that. It's being worked on. I will update the PR to explain this in the docs and ask users to add a new executor for js instead of using the regular blocking executor. It's not very nice, but like the restriction on using the sandbox, I hope that this will go away with new versions and the instructions can then be simplified.

mikehearn avatar May 24 '24 12:05 mikehearn

you could always define a separate executor for JS view rendering.

graemerocher avatar May 24 '24 14:05 graemerocher

I updated the docs to tell users to just disable Loom for the blocking executor. It's only a temporary limitation and should not be a big deal.

I started writing a guide, but it seems the infrastructure has a lot of dependencies on the feature being actually published. So I will go back to that after there's been a release with this in it, as then dependency resolution etc will work.

Do I need to update the starter system for the new feature or does that happen automatically?

mikehearn avatar May 24 '24 14:05 mikehearn

no starter will be need to be updated

graemerocher avatar May 24 '24 14:05 graemerocher