From 4e3ad5ba8df602c5cfb858b3cd6f9ca9fdd8e7c7 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 22 Jul 2024 15:43:27 +0300 Subject: [PATCH] tmp --- lib/react_on_rails/helper.rb | 18 ++++- .../react_component/render_options.rb | 4 + lib/react_on_rails/utils.rb | 45 +++++++---- node_package/src/ReactOnRails.ts | 9 +++ node_package/src/ReactOnRailsRSC.ts | 79 +++++++++++++++++++ node_package/src/types/index.ts | 1 + package.json | 12 ++- yarn.lock | 58 +++++++++----- 8 files changed, 187 insertions(+), 39 deletions(-) create mode 100644 node_package/src/ReactOnRailsRSC.ts diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 65d7061d13..cda7d22971 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -91,6 +91,15 @@ def react_component(component_name, options = {}) end end + def rsc_react_component(component_name, options = {}) + res = internal_rsc_react_component(component_name, options) + s = "" + res.each_chunk do |chunk| + s += chunk + end + s + end + def stream_react_component(component_name, options = {}) options = options.merge(stream?: true) result = internal_react_component(component_name, options) @@ -468,6 +477,13 @@ def prepend_render_rails_context(render_value) "#{rails_context_if_not_already_rendered}\n#{render_value}".html_safe end + def internal_rsc_react_component(react_component_name, options = {}) + options = options.merge(rsc?: true) + render_options = ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name, + options: options) + server_rendered_react_component(render_options) + end + def internal_react_component(react_component_name, options = {}) # Create the JavaScript and HTML to allow either client or server rendering of the # react_component. @@ -561,7 +577,7 @@ def server_rendered_react_component(render_options) end # TODO: handle errors for streams - return result if render_options.stream? + return result if render_options.stream? || render_options.rsc? if result["hasErrors"] && render_options.raise_on_prerender_error # We caught this exception on our backtrace handler diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index 17b40bf2b8..97576bcf33 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -107,6 +107,10 @@ def stream? options[:stream?] end + def rsc? + options[:rsc?] + end + private attr_reader :options diff --git a/lib/react_on_rails/utils.rb b/lib/react_on_rails/utils.rb index 7f3529b985..c668e12e45 100644 --- a/lib/react_on_rails/utils.rb +++ b/lib/react_on_rails/utils.rb @@ -66,7 +66,7 @@ def self.server_bundle_path_is_http? server_bundle_js_file_path =~ %r{https?://} end - def self.server_bundle_js_file_path + def self.bundle_js_file_path(bundle_name) # Either: # 1. Using same bundle for both server and client, so server bundle will be hashed in manifest # 2. Using a different bundle (different Webpack config), so file is not hashed, and @@ -76,12 +76,9 @@ def self.server_bundle_js_file_path # a. The webpack manifest plugin would have a race condition where the same manifest.json # is edited by both the webpack-dev-server # b. There is no good reason to hash the server bundle name. - return @server_bundle_path if @server_bundle_path && !Rails.env.development? - - bundle_name = ReactOnRails.configuration.server_bundle_js_file - @server_bundle_path = if ReactOnRails::WebpackerUtils.using_webpacker? + @server_bundle_path = if ReactOnRails::WebpackerUtils.using_webpacker? && bundle_name != "manifest.json" begin - bundle_js_file_path(bundle_name) + ReactOnRails::WebpackerUtils.bundle_js_uri_from_webpacker(bundle_name) rescue Webpacker::Manifest::MissingEntryError File.expand_path( File.join(ReactOnRails::WebpackerUtils.webpacker_public_output_path, @@ -89,19 +86,35 @@ def self.server_bundle_js_file_path ) end else - bundle_js_file_path(bundle_name) + # Default to the non-hashed name in the specified output directory, which, for legacy + # React on Rails, this is the output directory picked up by the asset pipeline. + # For Webpacker, this is the public output path defined in the webpacker.yml file. + File.join(generated_assets_full_path, bundle_name) end end - def self.bundle_js_file_path(bundle_name) - if ReactOnRails::WebpackerUtils.using_webpacker? && bundle_name != "manifest.json" - ReactOnRails::WebpackerUtils.bundle_js_uri_from_webpacker(bundle_name) - else - # Default to the non-hashed name in the specified output directory, which, for legacy - # React on Rails, this is the output directory picked up by the asset pipeline. - # For Webpacker, this is the public output path defined in the webpacker.yml file. - File.join(generated_assets_full_path, bundle_name) - end + def self.server_bundle_js_file_path + # Either: + # 1. Using same bundle for both server and client, so server bundle will be hashed in manifest + # 2. Using a different bundle (different Webpack config), so file is not hashed, and + # bundle_js_path will throw so the default path is used without a hash. + # 3. The third option of having the server bundle hashed and a different configuration than + # the client bundle is not supported for 2 reasons: + # a. The webpack manifest plugin would have a race condition where the same manifest.json + # is edited by both the webpack-dev-server + # b. There is no good reason to hash the server bundle name. + return @server_bundle_path if @server_bundle_path && !Rails.env.development? + + bundle_name = ReactOnRails.configuration.server_bundle_js_file + @server_bundle_path = bundle_js_file_path(bundle_name) + end + + def self.rsc_bundle_js_file_path + return @rsc_bundle_path if @rsc_bundle_path && !Rails.env.development? + + # TODO: make it configurable + bundle_name = "rsc-bundle.js" + @server_bundle_path = bundle_js_file_path(bundle_name) end def self.running_on_windows? diff --git a/node_package/src/ReactOnRails.ts b/node_package/src/ReactOnRails.ts index 10b72771db..8180ff664f 100644 --- a/node_package/src/ReactOnRails.ts +++ b/node_package/src/ReactOnRails.ts @@ -250,6 +250,15 @@ ctx.ReactOnRails = { return streamServerRenderedReactComponent(options); }, + /** + * Used by server rendering by Rails + * @param options + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + serverRenderRSCReactComponent(options: RenderParams): PassThrough { + throw new Error('serverRenderRSCReactComponent is supported in RSC bundle only.'); + }, + /** * Used by Rails to catch errors in rendering * @param options diff --git a/node_package/src/ReactOnRailsRSC.ts b/node_package/src/ReactOnRailsRSC.ts new file mode 100644 index 0000000000..1d1de30ae7 --- /dev/null +++ b/node_package/src/ReactOnRailsRSC.ts @@ -0,0 +1,79 @@ +import type { ReactElement } from 'react'; +// @ts-expect-error will define this module types later +import { renderToReadableStream } from 'react-server-dom-webpack/server.edge'; +import { PassThrough } from 'stream'; + +import { RenderParams } from './types'; +import ComponentRegistry from './ComponentRegistry'; +import createReactOutput from './createReactOutput'; +import { isPromise, isServerRenderHash } from './isServerRenderResult'; +import handleError from './handleError'; +import ReactOnRails from './ReactOnRails'; + +(async () => { + try { + // @ts-expect-error AsyncLocalStorage is not in the node types + globalThis.AsyncLocalStorage = (await import('node:async_hooks')).AsyncLocalStorage; + } catch (e) { + console.log('AsyncLocalStorage not found'); + } +})(); + +const stringToStream = (str: string) => { + const stream = new PassThrough(); + stream.push(str); + stream.push(null); + return stream; +}; + +ReactOnRails.serverRenderRSCReactComponent = (options: RenderParams) => { + const { name, domNodeId, trace, props, railsContext, throwJsErrors } = options; + + let renderResult: null | PassThrough = null; + + try { + const componentObj = ComponentRegistry.get(name); + if (componentObj.isRenderer) { + throw new Error(`\ +Detected a renderer while server rendering component '${name}'. \ +See https://github.com/shakacode/react_on_rails#renderer-functions`); + } + + const reactRenderingResult = createReactOutput({ + componentObj, + domNodeId, + trace, + props, + railsContext, + }); + + if (isServerRenderHash(reactRenderingResult) || isPromise(reactRenderingResult)) { + throw new Error('Server rendering of streams is not supported for server render hashes or promises.'); + } + + renderResult = new PassThrough(); + const streamReader = renderToReadableStream(reactRenderingResult as ReactElement).getReader(); + const processStream = async () => { + const { done, value } = await streamReader.read(); + if (done) { + renderResult?.push(null); + return; + } + + renderResult?.push(value); + processStream(); + } + processStream(); + } catch (e: unknown) { + if (throwJsErrors) { + throw e; + } + + renderResult = stringToStream(`Error: ${e}`); + } + + return renderResult; +}; + +export * from './types'; +export default ReactOnRails; diff --git a/node_package/src/types/index.ts b/node_package/src/types/index.ts index 878b8820d4..3701b8a5d9 100644 --- a/node_package/src/types/index.ts +++ b/node_package/src/types/index.ts @@ -139,6 +139,7 @@ export interface ReactOnRails { getComponent(name: string): RegisteredComponent; serverRenderReactComponent(options: RenderParams): null | string | Promise; streamServerRenderedReactComponent(options: RenderParams): PassThrough; + serverRenderRSCReactComponent(options: RenderParams): PassThrough; handleError(options: ErrorOptions): string | undefined; buildConsoleReplay(): string; registeredComponents(): Map; diff --git a/package.json b/package.json index bf46077629..6d57669ea0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,12 @@ "name": "react-on-rails", "version": "14.0.1", "description": "react-on-rails JavaScript for react_on_rails Ruby gem", - "main": "node_package/lib/ReactOnRails.js", + "exports": { + ".": { + "rsc-server": "./node_package/lib/ReactOnRailsRSC.js", + "default": "./node_package/lib/ReactOnRails.js" + } + }, "directories": { "doc": "docs" }, @@ -39,8 +44,9 @@ "prettier": "^2.8.8", "prettier-eslint-cli": "^5.0.0", "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "18.3.0-canary-670811593-20240322", + "react-dom": "18.3.0-canary-670811593-20240322", + "react-server-dom-webpack": "18.3.0-canary-670811593-20240322", "react-transform-hmr": "^1.0.4", "redux": "^4.2.1", "ts-jest": "^29.1.0", diff --git a/yarn.lock b/yarn.lock index b4663cb37b..2a787a18d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,6 +1972,13 @@ acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== +acorn-loose@^8.3.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.4.0.tgz#26d3e219756d1e180d006f5bcc8d261a28530f55" + integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== + dependencies: + acorn "^8.11.0" + acorn-walk@^8.0.2: version "8.3.1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.1.tgz#2f10f5b69329d90ae18c58bf1fa8fccd8b959a43" @@ -2002,6 +2009,11 @@ acorn@^8.1.0, acorn@^8.8.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +acorn@^8.11.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -5155,7 +5167,7 @@ loglevel@^1.4.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== -loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -5337,6 +5349,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +neo-async@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -5877,13 +5894,12 @@ react-deep-force-update@^1.0.0: resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-1.1.2.tgz#3d2ae45c2c9040cbb1772be52f8ea1ade6ca2ee1" integrity sha512-WUSQJ4P/wWcusaH+zZmbECOk7H5N2pOIl0vzheeornkIMhu+qrNdGFm0bDZLCb0hSF0jf/kH1SgkNGfBdTc4wA== -react-dom@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== +react-dom@18.3.0-canary-670811593-20240322: + version "18.3.0-canary-670811593-20240322" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.0-canary-670811593-20240322.tgz#ac677b164fd83050272bf985e740ed4ca65337be" + integrity sha512-AHxCnyDzZueXIHY4WA2Uba1yaL7/vbjhO3D3TWPQeruKD5MwgD0/xExZi0T104gBr6Thv6MEsLSxFjBAHhHKKg== dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" + scheduler "0.24.0-canary-670811593-20240322" react-is@^16.13.1: version "16.13.1" @@ -5903,6 +5919,14 @@ react-proxy@^1.1.7: lodash "^4.6.1" react-deep-force-update "^1.0.0" +react-server-dom-webpack@18.3.0-canary-670811593-20240322: + version "18.3.0-canary-670811593-20240322" + resolved "https://registry.yarnpkg.com/react-server-dom-webpack/-/react-server-dom-webpack-18.3.0-canary-670811593-20240322.tgz#e9b99b1f0179357e5acbf2fbacaee88dd1e8bf3b" + integrity sha512-YaCk3AvvOXcOo0FL7SlAY2GVBeuZKFQ/5FfAtE48IjpI6MvXTwMBu3QVnT/Ukk9Y4M9GzpIbLtuc8hPjfFAOaw== + dependencies: + acorn-loose "^8.3.0" + neo-async "^2.6.1" + react-transform-hmr@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/react-transform-hmr/-/react-transform-hmr-1.0.4.tgz#e1a40bd0aaefc72e8dfd7a7cda09af85066397bb" @@ -5911,12 +5935,10 @@ react-transform-hmr@^1.0.4: global "^4.3.0" react-proxy "^1.1.7" -react@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" +react@18.3.0-canary-670811593-20240322: + version "18.3.0-canary-670811593-20240322" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.0-canary-670811593-20240322.tgz#3735250b45468d313ed36121324452bb5a732e9b" + integrity sha512-EI6+q3tOT+0z4OkB2sz842Ra/n/yz7b3jOJhSK1HQwi4Ng29VJzLGngWmSuxQ94YfdE3EBhpUKDfgNgzoKM9Vg== readdirp@~3.6.0: version "3.6.0" @@ -6213,12 +6235,10 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" +scheduler@0.24.0-canary-670811593-20240322: + version "0.24.0-canary-670811593-20240322" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.24.0-canary-670811593-20240322.tgz#45c5c45f18a127ab4e3c805dd466bc231b20adf3" + integrity sha512-IGX6Fq969h1L0X7jV0sJ/EdI4fr+mRetbBNJl55nn+/RsCuQSVwgKnZG6Q3NByixDNbkRI8nRmWuhOm8NQowGQ== semver@5.5.0: version "5.5.0"