diff --git a/CHANGELOG.md b/CHANGELOG.md index 902f3be3ca..ecc79760ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ ### [Unreleased] +#### Changed + +- **[Pro]** **Pro generator now creates the Node Renderer at `renderer/node-renderer.js`**: The canonical location for the Node Renderer entry point is now a dedicated top-level `renderer/` directory instead of `client/`, making it straightforward to exclude from production Docker builds that strip JS sources after bundling. Docs and Pro `spec/dummy` now use the new path consistently. Existing apps are unaffected — the generator skips files that already exist (including a legacy `client/node-renderer.js`). Fixes [Issue 3073](https://github.com/shakacode/react_on_rails/issues/3073). [PR 3165](https://github.com/shakacode/react_on_rails/pull/3165) by [justin808](https://github.com/justin808). + #### Fixed - **Client startup now recovers if initialization begins during `interactive` after `DOMContentLoaded` already fired**: React on Rails now still initializes the page when the client bundle starts in the browser timing window after `DOMContentLoaded` but before the document reaches `complete`. Fixes [Issue 3150](https://github.com/shakacode/react_on_rails/issues/3150). [PR 3151](https://github.com/shakacode/react_on_rails/pull/3151) by [ihabadham](https://github.com/ihabadham). diff --git a/benchmarks/bench-node-renderer.rb b/benchmarks/bench-node-renderer.rb index b994cbb239..306bfc4fe0 100755 --- a/benchmarks/bench-node-renderer.rb +++ b/benchmarks/bench-node-renderer.rb @@ -23,7 +23,7 @@ def read_protocol_version def read_password_from_config config_path = File.expand_path( - "../react_on_rails_pro/spec/dummy/client/node-renderer.js", + "../react_on_rails_pro/spec/dummy/renderer/node-renderer.js", __dir__ ) config_content = File.read(config_path) diff --git a/docs/oss/api-reference/generator-details.md b/docs/oss/api-reference/generator-details.md index 96d02c5df4..6592f936a0 100644 --- a/docs/oss/api-reference/generator-details.md +++ b/docs/oss/api-reference/generator-details.md @@ -50,7 +50,7 @@ can pass the redux option if you'd like to have redux setup for you automaticall Passing the --pro generator option sets up React on Rails Pro with Node server rendering, fragment caching, and code-splitting support. Requires the react_on_rails_pro gem (add it to your Gemfile first). - Creates the Pro initializer, node-renderer.js, and adds the Node Renderer + Creates the Pro initializer, renderer/node-renderer.js, and adds the Node Renderer process to Procfile.dev. * RSC (React Server Components) @@ -215,7 +215,7 @@ rails generate react_on_rails:install --pro **What gets created:** - `config/initializers/react_on_rails_pro.rb` - Pro configuration with Node Renderer settings -- `client/node-renderer.js` - Node Renderer bootstrap file +- `renderer/node-renderer.js` - Node Renderer bootstrap file - Node Renderer process added to `Procfile.dev` - Pro npm packages (`react-on-rails-pro`, `react-on-rails-pro-node-renderer`) diff --git a/docs/oss/building-features/code-splitting.md b/docs/oss/building-features/code-splitting.md index 7120f1282e..9f085abbe3 100644 --- a/docs/oss/building-features/code-splitting.md +++ b/docs/oss/building-features/code-splitting.md @@ -252,7 +252,7 @@ in your webpack configuration. That turns off the polyfills for things like `__d ### Node Renderer -In your `node-renderer.js` file which runs node renderer, you need to specify `supportModules` options as follows: +In your `renderer/node-renderer.js` file which runs node renderer, you need to specify `supportModules` options as follows: ```js const path = require('path'); diff --git a/docs/oss/building-features/node-renderer/basics.md b/docs/oss/building-features/node-renderer/basics.md index 7c0f3cf719..dccfd57443 100644 --- a/docs/oss/building-features/node-renderer/basics.md +++ b/docs/oss/building-features/node-renderer/basics.md @@ -27,7 +27,7 @@ See the [Memory Leaks guide](../../../pro/js-memory-leaks.md) for common leak pa **node-renderer** is a standalone Node application to serve React SSR requests from a **Rails** client. You don't need any **Ruby** code to setup and launch it. You can configure with the command line or with a launch file. -> **Generator shortcut:** Running `rails generate react_on_rails:install --pro` (or `rails generate react_on_rails:pro` for existing apps) automatically creates `client/node-renderer.js`, adds the Node Renderer process to `Procfile.dev`, and installs the required npm packages. See [Installation](../../../pro/installation.md) for details. The manual setup below is for apps that need custom configuration. +> **Generator shortcut:** Running `rails generate react_on_rails:install --pro` (or `rails generate react_on_rails:pro` for existing apps) automatically creates `renderer/node-renderer.js`, adds the Node Renderer process to `Procfile.dev`, and installs the required npm packages. See [Installation](../../../pro/installation.md) for details. The manual setup below is for apps that need custom configuration. ## Simple Command Line for node-renderer @@ -65,7 +65,7 @@ For the most control over the setup, create a JavaScript file to start the NodeR # or: yarn add react-on-rails-pro-node-renderer # or: bun add react-on-rails-pro-node-renderer ``` -4. Configure a JavaScript file that will launch the rendering server per the docs in [Node Renderer JavaScript Configuration](./js-configuration.md). For example, create a file `node-renderer.js`. Here is a simple example that uses all the defaults except for serverBundleCachePath: +4. Configure a JavaScript file that will launch the rendering server per the docs in [Node Renderer JavaScript Configuration](./js-configuration.md). For example, create a file `renderer/node-renderer.js`. Here is a simple example that uses all the defaults except for serverBundleCachePath: ```javascript import path from 'path'; @@ -78,7 +78,7 @@ For the most control over the setup, create a JavaScript file to start the NodeR reactOnRailsProNodeRenderer(config); ``` -5. Now you can launch your renderer server with `node node-renderer.js`. You will probably add a script to your `package.json`. +5. Now you can launch your renderer server with `node renderer/node-renderer.js`. You will probably add a script to your `package.json`. 6. You can use a command line argument of `-p SOME_PORT` to override any configured or ENV value for the port. ## Setup Rails Application @@ -163,7 +163,7 @@ jobs: steps: - name: Start Node Renderer run: | - node client/node-renderer.js & + node renderer/node-renderer.js & # Wait for the renderer to be ready. # The renderer uses cleartext HTTP/2 (h2c), so use --http2-prior-knowledge for the probe. # --max-time 2 prevents hangs if the port is open but the process is stalled. diff --git a/docs/oss/building-features/node-renderer/container-deployment.md b/docs/oss/building-features/node-renderer/container-deployment.md index c111e093fe..2afe3b2529 100644 --- a/docs/oss/building-features/node-renderer/container-deployment.md +++ b/docs/oss/building-features/node-renderer/container-deployment.md @@ -131,6 +131,8 @@ end ## Dockerfile Example +> **Why the renderer entry point lives in a dedicated `renderer/` directory:** Production Docker builds commonly strip JavaScript sources after the client bundles are built, since the Rails app no longer needs them at runtime. Keeping the renderer entry point in its own top-level directory (separate from `client/`) makes it trivial to exclude from that cleanup — the Node Renderer process still needs its entry file and dependencies at runtime. + A minimal Dockerfile that bundles Rails and the Node Renderer in a single image: ```dockerfile @@ -175,10 +177,10 @@ For the single-container pattern, use a process manager like [overmind](https:// ```text # Procfile rails: bundle exec rails server -b 0.0.0.0 -p 3000 -renderer: node client/node-renderer.js +renderer: node renderer/node-renderer.js ``` -> **Tip:** For sidecar containers, use the same image but override the `CMD` — one container runs `bundle exec rails server`, the other runs `node client/node-renderer.js` (or your Node Renderer entry point). +> **Tip:** For sidecar containers, use the same image but override the `CMD` — one container runs `bundle exec rails server`, the other runs `node renderer/node-renderer.js` (or your Node Renderer entry point). ## Docker Compose Example @@ -199,7 +201,7 @@ services: renderer: build: . - command: node client/node-renderer.js + command: node renderer/node-renderer.js ports: - '3800:3800' environment: @@ -220,7 +222,7 @@ services: By default, the Node Renderer binds to `localhost`. For **sidecar containers** in the same Kubernetes pod, that works because the containers share a network namespace. For **separate workloads** or Docker Compose setups without shared networking, bind to `0.0.0.0`: ```javascript -// node-renderer.js +// renderer/node-renderer.js import { reactOnRailsProNodeRenderer } from 'react-on-rails-pro-node-renderer'; const config = { @@ -488,7 +490,7 @@ spec: - name: node-renderer image: your-app:latest # Same image as Rails - command: ['node', 'client/node-renderer.js'] + command: ['node', 'renderer/node-renderer.js'] ports: - containerPort: 3800 env: diff --git a/docs/oss/building-features/node-renderer/debugging.md b/docs/oss/building-features/node-renderer/debugging.md index e736e12f7d..956b2fe0e7 100644 --- a/docs/oss/building-features/node-renderer/debugging.md +++ b/docs/oss/building-features/node-renderer/debugging.md @@ -63,7 +63,7 @@ Use Node's built-in flag to write heap snapshots on demand: ```bash cd react_on_rails_pro/spec/dummy # Adjust the port if your Rails app points at a different renderer URL. -NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2" RENDERER_PORT=3800 node client/node-renderer.js +NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2" RENDERER_PORT=3800 node renderer/node-renderer.js ``` Then capture snapshots at different times: diff --git a/docs/oss/building-features/node-renderer/heroku.md b/docs/oss/building-features/node-renderer/heroku.md index 08836e4b5b..ee397396e4 100644 --- a/docs/oss/building-features/node-renderer/heroku.md +++ b/docs/oss/building-features/node-renderer/heroku.md @@ -46,7 +46,7 @@ Define the script in your root `package.json` so Heroku can run it from the app ```json { "scripts": { - "node-renderer": "node client/node-renderer.js" + "node-renderer": "node renderer/node-renderer.js" } } ``` diff --git a/docs/oss/building-features/node-renderer/js-configuration.md b/docs/oss/building-features/node-renderer/js-configuration.md index d524c424b9..08fd305766 100644 --- a/docs/oss/building-features/node-renderer/js-configuration.md +++ b/docs/oss/building-features/node-renderer/js-configuration.md @@ -58,12 +58,12 @@ Deprecated options: ### Testing example: -[spec/dummy/client/node-renderer.js](https://github.com/shakacode/react_on_rails/blob/main/react_on_rails_pro/spec/dummy/client/node-renderer.js) +[react_on_rails_pro/spec/dummy/renderer/node-renderer.js](../../../../react_on_rails_pro/spec/dummy/renderer/node-renderer.js) ### Simple example: -Create a file `client/node-renderer.js`. The generator uses this filename and CommonJS syntax so -the file runs directly with `node client/node-renderer.js` without extra ESM configuration. +Create a file `renderer/node-renderer.js`. The generator uses this filename and CommonJS syntax so +the file runs directly with `node renderer/node-renderer.js` without extra ESM configuration. ```js const path = require('path'); @@ -94,7 +94,7 @@ And add a root-level script to the `scripts` section of your `package.json` ```json "scripts": { - "node-renderer": "node client/node-renderer.js" + "node-renderer": "node renderer/node-renderer.js" }, ``` @@ -113,7 +113,7 @@ For advanced use cases, you can customize the Fastify server instance by importi When running the node-renderer in Docker or Kubernetes, you may need a `/health` endpoint for container health checks: The advanced examples below use ES modules for readability. If you want this file to keep running -as `node client/node-renderer.js`, either keep using the CommonJS pattern shown in the simple +as `node renderer/node-renderer.js`, either keep using the CommonJS pattern shown in the simple example above or switch the file to `.mjs` or `"type": "module"`. ```js diff --git a/docs/oss/deployment/docker-deployment.md b/docs/oss/deployment/docker-deployment.md index 0d69b009c5..71daaa0748 100644 --- a/docs/oss/deployment/docker-deployment.md +++ b/docs/oss/deployment/docker-deployment.md @@ -411,7 +411,7 @@ containers: - name: node-renderer image: your-registry/myapp-node-renderer:latest # must include Node.js - command: ['node', 'node-renderer.js'] + command: ['node', 'renderer/node-renderer.js'] ports: - containerPort: 3800 env: diff --git a/docs/oss/migrating/rsc-preparing-app.md b/docs/oss/migrating/rsc-preparing-app.md index 2c2a09c1f1..aa78ddd830 100644 --- a/docs/oss/migrating/rsc-preparing-app.md +++ b/docs/oss/migrating/rsc-preparing-app.md @@ -338,7 +338,7 @@ rails: rails s -p 3000 webpack-dev-server: HMR=true bin/shakapacker-dev-server rails-server-assets: HMR=true SERVER_BUNDLE_ONLY=true bin/shakapacker --watch rails-rsc-assets: HMR=true RSC_BUNDLE_ONLY=true bin/shakapacker --watch -node-renderer: node client/node-renderer.js +node-renderer: node renderer/node-renderer.js ``` > **For full webpack configuration details**, including the technical background on how the RSC loader, plugin, and manifests work together, see [How React Server Components Work](../../pro/react-server-components/how-react-server-components-work.md). diff --git a/docs/oss/migrating/rsc-troubleshooting.md b/docs/oss/migrating/rsc-troubleshooting.md index 66b27c010b..697de03b7d 100644 --- a/docs/oss/migrating/rsc-troubleshooting.md +++ b/docs/oss/migrating/rsc-troubleshooting.md @@ -816,7 +816,7 @@ for the current recommendation. See also **Fix:** Enable `supportModules` in your node renderer configuration to inject common Node.js globals: ```js -// node-renderer.js (or wherever you configure the renderer) +// renderer/node-renderer.js (or wherever you configure the renderer) module.exports = { supportModules: true, // Injects: Buffer, TextDecoder, TextEncoder, // URLSearchParams, ReadableStream, process, diff --git a/docs/pro/installation.md b/docs/pro/installation.md index 0e537cd6c6..c1b5b3b5be 100644 --- a/docs/pro/installation.md +++ b/docs/pro/installation.md @@ -34,7 +34,7 @@ git commit -m "Prepare app for React on Rails Pro install" bundle exec rails generate react_on_rails:install --pro ``` -This creates the Pro initializer, node-renderer.js, installs npm packages, and adds the Node Renderer to Procfile.dev. +This creates the Pro initializer, `renderer/node-renderer.js`, installs npm packages, and adds the Node Renderer to Procfile.dev. ## Upgrading an Existing App @@ -227,7 +227,7 @@ yarn add react-on-rails-pro-node-renderer@ --exact ## Node Renderer Setup -Create a JavaScript file to configure and launch the node renderer, for example `react-on-rails-pro-node-renderer.js`: +Create a JavaScript file to configure and launch the node renderer at `renderer/node-renderer.js`: ```js const path = require('path'); @@ -276,7 +276,7 @@ Add a script to your `package.json`: ```json { "scripts": { - "node-renderer": "node ./react-on-rails-pro-node-renderer.js" + "node-renderer": "node renderer/node-renderer.js" } } ``` diff --git a/docs/pro/js-memory-leaks.md b/docs/pro/js-memory-leaks.md index 523da91234..ce358bf10b 100644 --- a/docs/pro/js-memory-leaks.md +++ b/docs/pro/js-memory-leaks.md @@ -141,7 +141,7 @@ done Node provides a built-in flag that writes heap snapshots on a signal — no custom code required: ```bash -NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2" node node-renderer.js +NODE_OPTIONS="--heapsnapshot-signal=SIGUSR2" node renderer/node-renderer.js ``` Then send `kill -USR2 ` at different times to capture snapshots. Each signal writes a `.heapsnapshot` file to the working directory. @@ -186,7 +186,7 @@ To diagnose leaks in a running container: Start the renderer with the `--inspect` flag to connect Chrome DevTools: ```bash -node --inspect node-renderer.js +node --inspect renderer/node-renderer.js ``` Open `chrome://inspect` in Chrome, take heap snapshots, and use the "Comparison" view to see what objects accumulated between snapshots. @@ -200,7 +200,7 @@ Without this flag, V8 reads the container's memory limit and sets a very large h **Always set this for production:** ```bash -NODE_OPTIONS=--max-old-space-size=1536 node node-renderer.js +NODE_OPTIONS=--max-old-space-size=1536 node renderer/node-renderer.js ``` Size it based on your container memory and worker count. For example, with 4GB container memory and 3 workers: `4096 / 3 ≈ 1365`, round to `1400`. diff --git a/docs/pro/profiling-server-side-rendering-code.md b/docs/pro/profiling-server-side-rendering-code.md index e2701936a6..bb2cb51fe2 100644 --- a/docs/pro/profiling-server-side-rendering-code.md +++ b/docs/pro/profiling-server-side-rendering-code.md @@ -28,7 +28,7 @@ installed. On macOS, you can install it with `brew install overmind`. ```bash cd react_on_rails_pro/spec/dummy - RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node --inspect client/node-renderer.js + RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node --inspect renderer/node-renderer.js ``` Keep this terminal open while you profile. If your app usually starts the renderer through a diff --git a/packages/react-on-rails-pro-node-renderer/README.md b/packages/react-on-rails-pro-node-renderer/README.md index 8a1a6ffe5d..5a053aa66c 100644 --- a/packages/react-on-rails-pro-node-renderer/README.md +++ b/packages/react-on-rails-pro-node-renderer/README.md @@ -18,7 +18,7 @@ pnpm add react-on-rails-pro-node-renderer ### 1. Create the Node Renderer entry file -Create `node-renderer.js` in your project root: +Create `renderer/node-renderer.js` in your project root: ```js const path = require('path'); @@ -72,13 +72,13 @@ libraryTarget: 'commonjs2', ### 4. Start the renderer ```bash -node node-renderer.js +node renderer/node-renderer.js ``` Or add to your `Procfile.dev`: ```text -node-renderer: node node-renderer.js +node-renderer: node renderer/node-renderer.js ``` ## Generator (Recommended) diff --git a/react_on_rails/lib/generators/react_on_rails/base_generator.rb b/react_on_rails/lib/generators/react_on_rails/base_generator.rb index ff23314b09..9a50354c24 100644 --- a/react_on_rails/lib/generators/react_on_rails/base_generator.rb +++ b/react_on_rails/lib/generators/react_on_rails/base_generator.rb @@ -418,7 +418,7 @@ def home_page_file_hints if use_pro? hints << { - path: "client/node-renderer.js", + path: "renderer/node-renderer.js", description: "Node renderer entrypoint used for Pro SSR and RSC." } end diff --git a/react_on_rails/lib/generators/react_on_rails/demo_page_config.rb b/react_on_rails/lib/generators/react_on_rails/demo_page_config.rb index 75f45e6b85..be3f12a2a7 100644 --- a/react_on_rails/lib/generators/react_on_rails/demo_page_config.rb +++ b/react_on_rails/lib/generators/react_on_rails/demo_page_config.rb @@ -182,7 +182,7 @@ def hello_server_file_hints description: "Rails view that calls stream_react_component." }, { - path: "client/node-renderer.js", + path: "renderer/node-renderer.js", description: "Node renderer entrypoint used by the Pro SSR and RSC stack." } ] diff --git a/react_on_rails/lib/generators/react_on_rails/pro/USAGE b/react_on_rails/lib/generators/react_on_rails/pro/USAGE index 666eb35a0f..15471481ff 100644 --- a/react_on_rails/lib/generators/react_on_rails/pro/USAGE +++ b/react_on_rails/lib/generators/react_on_rails/pro/USAGE @@ -6,7 +6,7 @@ Example: This will add: - Pro initializer (config/initializers/react_on_rails_pro.rb) - - Node renderer (client/node-renderer.js) + - Node renderer (renderer/node-renderer.js) - Node renderer process to Procfile.dev Modifies: diff --git a/react_on_rails/lib/generators/react_on_rails/pro_setup.rb b/react_on_rails/lib/generators/react_on_rails/pro_setup.rb index b84301eed7..4242d8def0 100644 --- a/react_on_rails/lib/generators/react_on_rails/pro_setup.rb +++ b/react_on_rails/lib/generators/react_on_rails/pro_setup.rb @@ -33,7 +33,7 @@ module ProSetup # # Creates: # - config/initializers/react_on_rails_pro.rb - # - client/node-renderer.js + # - renderer/node-renderer.js # - Procfile.dev entry for node-renderer # # @note NPM dependencies are handled separately by JsDependencyManager @@ -43,8 +43,8 @@ def setup_pro say set_color("=" * 80, :cyan) create_pro_initializer - create_node_renderer - add_pro_to_procfile + legacy_renderer_detected = create_node_renderer + add_pro_to_procfile unless legacy_renderer_detected update_webpack_config_for_pro say set_color("=" * 80, :cyan) @@ -210,23 +210,64 @@ def create_pro_initializer say "✅ Created #{initializer_path}", :green end + # Matches active (uncommented) Procfile.dev node-renderer lines, tolerating + # an optional `./` prefix that a user may have added by hand + # (e.g. `node ./renderer/node-renderer.js`). + NEW_RENDERER_COMMAND_REGEX = %r{^[ \t]*node-renderer:[^\n]*\bnode\s+\.?/?renderer/node-renderer\.js\b} + LEGACY_RENDERER_COMMAND_REGEX = %r{^[ \t]*node-renderer:[^\n]*\bnode\s+\.?/?client/node-renderer\.js\b} + + # Creates renderer/node-renderer.js unless either the new path or the legacy + # client/node-renderer.js already exists. + # + # @return [Boolean] true when a legacy client/node-renderer.js was detected + # (caller should skip add_pro_to_procfile to avoid pointing Procfile.dev + # at a file that wasn't created); false otherwise. def create_node_renderer - node_renderer_path = "client/node-renderer.js" + node_renderer_path = "renderer/node-renderer.js" + legacy_node_renderer_path = "client/node-renderer.js" if File.exist?(File.join(destination_root, node_renderer_path)) say "ℹ️ #{node_renderer_path} already exists, skipping", :yellow - return + return false + end + + if File.exist?(File.join(destination_root, legacy_node_renderer_path)) + say "ℹ️ #{legacy_node_renderer_path} detected, keeping existing renderer; " \ + "to migrate, move it to #{node_renderer_path} and update any references " \ + "(e.g. Procfile.dev, Procfile.prod, Docker CMD / command):", :yellow + say " node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node #{node_renderer_path}", :yellow + warn_on_stale_legacy_procfile_entry + return true end say "📝 Creating Node Renderer bootstrap...", :yellow - # Ensure client directory exists - FileUtils.mkdir_p(File.join(destination_root, "client")) + empty_directory("renderer") - template_path = "templates/pro/base/client/node-renderer.js" + template_path = "templates/pro/base/renderer/node-renderer.js" copy_file(template_path, node_renderer_path) say "✅ Created #{node_renderer_path}", :green + false + end + + # When a legacy client/node-renderer.js is detected, add_pro_to_procfile is + # skipped, so surface a pointed warning if Procfile.dev still launches the + # legacy entry. This nudges the user to update the exact line they need to + # touch rather than leaving them to diff the generic migration hint against + # their Procfile themselves. + def warn_on_stale_legacy_procfile_entry + procfile_path = File.join(destination_root, "Procfile.dev") + return unless File.exist?(procfile_path) + + procfile_content = File.read(procfile_path) + return unless procfile_content.match?(LEGACY_RENDERER_COMMAND_REGEX) + + GeneratorMessages.add_warning(<<~MSG.strip) + ⚠️ Procfile.dev still launches the legacy client/node-renderer.js. + After migrating the renderer file, update that line to: + node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js + MSG end def add_pro_to_procfile @@ -237,22 +278,31 @@ def add_pro_to_procfile ⚠️ Procfile.dev not found. Skipping Node Renderer process addition. You'll need to add the Node Renderer to your process manager manually: - node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node client/node-renderer.js + node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js MSG return end - if File.read(procfile_path).include?("node-renderer:") + procfile_content = File.read(procfile_path) + + if procfile_content.match?(NEW_RENDERER_COMMAND_REGEX) say "ℹ️ Node Renderer already in Procfile.dev, skipping", :yellow return end + if procfile_content.match?(/^[ \t]*node-renderer:/) + say "⚠️ Procfile.dev has a node-renderer: entry that doesn't reference " \ + "renderer/node-renderer.js. Update it manually to:", :yellow + say " node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js", :yellow + return + end + say "📝 Adding Node Renderer to Procfile.dev...", :yellow node_renderer_line = <<~PROCFILE # React on Rails Pro - Node Renderer for SSR - node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node client/node-renderer.js + node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js PROCFILE append_to_file("Procfile.dev", node_renderer_line) diff --git a/react_on_rails/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt b/react_on_rails/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt index d976d9bd3e..f1a3e416fe 100644 --- a/react_on_rails/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +++ b/react_on_rails/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt @@ -6,7 +6,7 @@ ReactOnRailsPro.configure do |config| config.server_renderer = "NodeRenderer" config.renderer_url = ENV.fetch("REACT_RENDERER_URL", "http://localhost:3800") - # See value in client/node-renderer.js + # See value in renderer/node-renderer.js config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword") config.ssr_timeout = 5 diff --git a/react_on_rails/lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js b/react_on_rails/lib/generators/react_on_rails/templates/pro/base/renderer/node-renderer.js similarity index 95% rename from react_on_rails/lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js rename to react_on_rails/lib/generators/react_on_rails/templates/pro/base/renderer/node-renderer.js index f26bb0076a..83ae2c789c 100644 --- a/react_on_rails/lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js +++ b/react_on_rails/lib/generators/react_on_rails/templates/pro/base/renderer/node-renderer.js @@ -6,6 +6,7 @@ const configuredWorkersCount = parseWorkersCount(env.RENDERER_WORKERS_COUNT) ?? parseWorkersCount(env.NODE_RENDERER_CONCURRENCY); const config = { + // Resolves to /.node-renderer-bundles (one level up from renderer/). serverBundleCachePath: path.resolve(__dirname, '../.node-renderer-bundles'), port: Number(env.RENDERER_PORT) || 3800, logLevel: env.RENDERER_LOG_LEVEL || 'info', diff --git a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb index 9c09bb1aa8..a0fce2f075 100644 --- a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb @@ -1316,7 +1316,7 @@ class ActiveSupport::TestCase end it "creates node-renderer.js bootstrap file" do - assert_file "client/node-renderer.js" do |content| + assert_file "renderer/node-renderer.js" do |content| expect(content).to include("reactOnRailsProNodeRenderer") expect(content).to include("require('react-on-rails-pro-node-renderer')") expect(content).to include("serverBundleCachePath") @@ -1332,7 +1332,7 @@ class ActiveSupport::TestCase assert_file "Procfile.dev" do |content| expect(content).to include("node-renderer:") expect(content).to include("RENDERER_PORT=3800") - expect(content).to include("node client/node-renderer.js") + expect(content).to include("node renderer/node-renderer.js") end end @@ -1497,13 +1497,13 @@ class ActiveSupport::TestCase context "when node-renderer.js already exists" do before(:all) do run_generator_test_with_args(%w[--pro], package_json: true) do - simulate_existing_dir("client") - simulate_existing_file("client/node-renderer.js", "// existing node-renderer\n") + simulate_existing_dir("renderer") + simulate_existing_file("renderer/node-renderer.js", "// existing node-renderer\n") end end it "does not overwrite existing node-renderer.js" do - assert_file "client/node-renderer.js" do |content| + assert_file "renderer/node-renderer.js" do |content| expect(content).to include("// existing node-renderer") expect(content).not_to include("reactOnRailsProNodeRenderer") end @@ -1550,7 +1550,7 @@ class ActiveSupport::TestCase include_examples "rsc_common_files" it "creates node-renderer.js" do - assert_file "client/node-renderer.js" do |content| + assert_file "renderer/node-renderer.js" do |content| expect(content).to include("reactOnRailsProNodeRenderer") expect(content).to include("require('react-on-rails-pro-node-renderer')") end @@ -1733,7 +1733,7 @@ class ActiveSupport::TestCase end it "creates node-renderer.js" do - assert_file "client/node-renderer.js" do |content| + assert_file "renderer/node-renderer.js" do |content| expect(content).to include("reactOnRailsProNodeRenderer") expect(content).to include("require('react-on-rails-pro-node-renderer')") end diff --git a/react_on_rails/spec/react_on_rails/generators/pro_generator_spec.rb b/react_on_rails/spec/react_on_rails/generators/pro_generator_spec.rb index 91e3b32833..2bca163b34 100644 --- a/react_on_rails/spec/react_on_rails/generators/pro_generator_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/pro_generator_spec.rb @@ -1223,6 +1223,313 @@ end end + context "when prerequisites are met and a legacy client/node-renderer.js exists" do + let(:legacy_renderer_content) { "// customized legacy renderer\n" } + + before do + prepare_destination + simulate_existing_rails_files(package_json: true) + simulate_existing_file("Gemfile", <<~RUBY) + source "https://rubygems.org" + gem "react_on_rails_pro" + RUBY + simulate_npm_files(package_json: true) + simulate_existing_file("config/initializers/react_on_rails.rb", "ReactOnRails.configure {}") + simulate_existing_file("Procfile.dev", "rails: bin/rails s\n") + simulate_base_webpack_files + simulate_existing_file("client/node-renderer.js", legacy_renderer_content) + allow(Gem).to receive(:loaded_specs).and_return({ "react_on_rails_pro" => double }) + + Dir.chdir(destination_root) do + run_generator(["--force"]) + end + end + + it "does not create renderer/node-renderer.js" do + expect(File.exist?(File.join(destination_root, "renderer/node-renderer.js"))).to be false + end + + it "preserves the legacy client/node-renderer.js" do + expect(File.read(File.join(destination_root, "client/node-renderer.js"))).to eq(legacy_renderer_content) + end + + it "does not add a node-renderer entry to Procfile.dev" do + expect(File.read(File.join(destination_root, "Procfile.dev"))).not_to include("node-renderer:") + end + end + + context "when renderer/node-renderer.js already exists but Procfile.dev lacks node-renderer entry" do + let(:existing_renderer_content) { "// existing renderer\n" } + + before do + prepare_destination + simulate_existing_rails_files(package_json: true) + simulate_existing_file("Gemfile", <<~RUBY) + source "https://rubygems.org" + gem "react_on_rails_pro" + RUBY + simulate_npm_files(package_json: true) + simulate_existing_file("config/initializers/react_on_rails.rb", "ReactOnRails.configure {}") + simulate_existing_file("Procfile.dev", "rails: bin/rails s\n") + simulate_base_webpack_files + simulate_existing_file("renderer/node-renderer.js", existing_renderer_content) + allow(Gem).to receive(:loaded_specs).and_return({ "react_on_rails_pro" => double }) + + Dir.chdir(destination_root) do + run_generator(["--force"]) + end + end + + it "preserves the existing renderer/node-renderer.js" do + expect(File.read(File.join(destination_root, "renderer/node-renderer.js"))).to eq(existing_renderer_content) + end + + it "adds the node-renderer entry to Procfile.dev" do + expect(File.read(File.join(destination_root, "Procfile.dev"))) + .to include("node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js") + end + end + + context "when renderer/node-renderer.js exists and Procfile.dev has a stale legacy node-renderer entry" do + let(:existing_renderer_content) { "// existing renderer\n" } + let(:stale_procfile) do + "rails: bin/rails s\n" \ + "node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node client/node-renderer.js\n" + end + + before do + prepare_destination + simulate_existing_rails_files(package_json: true) + simulate_existing_file("Gemfile", <<~RUBY) + source "https://rubygems.org" + gem "react_on_rails_pro" + RUBY + simulate_npm_files(package_json: true) + simulate_existing_file("config/initializers/react_on_rails.rb", "ReactOnRails.configure {}") + simulate_existing_file("Procfile.dev", stale_procfile) + simulate_base_webpack_files + simulate_existing_file("renderer/node-renderer.js", existing_renderer_content) + allow(Gem).to receive(:loaded_specs).and_return({ "react_on_rails_pro" => double }) + + Dir.chdir(destination_root) do + run_generator(["--force"]) + end + end + + it "does not append a second node-renderer entry to Procfile.dev" do + expect(File.read(File.join(destination_root, "Procfile.dev")).scan(/^node-renderer:/).size).to eq(1) + end + + it "leaves the stale legacy entry untouched" do + expect(File.read(File.join(destination_root, "Procfile.dev"))) + .to include("node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node client/node-renderer.js") + end + end + + context "when Procfile.dev already contains the new renderer/node-renderer.js entry" do + let(:existing_renderer_content) { "// existing renderer\n" } + let(:current_procfile) do + "rails: bin/rails s\n" \ + "node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js\n" + end + + before do + prepare_destination + simulate_existing_rails_files(package_json: true) + simulate_existing_file("Gemfile", <<~RUBY) + source "https://rubygems.org" + gem "react_on_rails_pro" + RUBY + simulate_npm_files(package_json: true) + simulate_existing_file("config/initializers/react_on_rails.rb", "ReactOnRails.configure {}") + simulate_existing_file("Procfile.dev", current_procfile) + simulate_base_webpack_files + simulate_existing_file("renderer/node-renderer.js", existing_renderer_content) + allow(Gem).to receive(:loaded_specs).and_return({ "react_on_rails_pro" => double }) + + Dir.chdir(destination_root) do + run_generator(["--force"]) + end + end + + it "leaves Procfile.dev unchanged" do + expect(File.read(File.join(destination_root, "Procfile.dev"))).to eq(current_procfile) + end + end + + context "when Procfile.dev uses a ./ prefix on the renderer command" do + let(:existing_renderer_content) { "// existing renderer\n" } + let(:current_procfile) do + "rails: bin/rails s\n" \ + "node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node ./renderer/node-renderer.js\n" + end + + before do + prepare_destination + simulate_existing_rails_files(package_json: true) + simulate_existing_file("Gemfile", <<~RUBY) + source "https://rubygems.org" + gem "react_on_rails_pro" + RUBY + simulate_npm_files(package_json: true) + simulate_existing_file("config/initializers/react_on_rails.rb", "ReactOnRails.configure {}") + simulate_existing_file("Procfile.dev", current_procfile) + simulate_base_webpack_files + simulate_existing_file("renderer/node-renderer.js", existing_renderer_content) + allow(Gem).to receive(:loaded_specs).and_return({ "react_on_rails_pro" => double }) + + Dir.chdir(destination_root) do + run_generator(["--force"]) + end + end + + it "treats the ./-prefixed command as already present and leaves Procfile.dev unchanged" do + expect(File.read(File.join(destination_root, "Procfile.dev"))).to eq(current_procfile) + end + end + + context "when Procfile.dev has only a commented-out renderer entry" do + let(:existing_renderer_content) { "// existing renderer\n" } + let(:commented_procfile) do + "rails: bin/rails s\n" \ + "# node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js\n" + end + + before do + prepare_destination + simulate_existing_rails_files(package_json: true) + simulate_existing_file("Gemfile", <<~RUBY) + source "https://rubygems.org" + gem "react_on_rails_pro" + RUBY + simulate_npm_files(package_json: true) + simulate_existing_file("config/initializers/react_on_rails.rb", "ReactOnRails.configure {}") + simulate_existing_file("Procfile.dev", commented_procfile) + simulate_base_webpack_files + simulate_existing_file("renderer/node-renderer.js", existing_renderer_content) + allow(Gem).to receive(:loaded_specs).and_return({ "react_on_rails_pro" => double }) + + Dir.chdir(destination_root) do + run_generator(["--force"]) + end + end + + it "adds a live node-renderer entry instead of treating the comment as configured" do + procfile = File.read(File.join(destination_root, "Procfile.dev")) + expect(procfile.scan(/^[ \t]*node-renderer:/).size).to eq(1) + expect(procfile) + .to include("node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js") + end + end + + context "when legacy client/node-renderer.js exists and Procfile.dev still launches it" do + let(:legacy_renderer_content) { "// customized legacy renderer\n" } + let(:stale_procfile) do + "rails: bin/rails s\n" \ + "node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node client/node-renderer.js\n" + end + + before do + prepare_destination + simulate_existing_rails_files(package_json: true) + simulate_existing_file("Gemfile", <<~RUBY) + source "https://rubygems.org" + gem "react_on_rails_pro" + RUBY + simulate_npm_files(package_json: true) + simulate_existing_file("config/initializers/react_on_rails.rb", "ReactOnRails.configure {}") + simulate_existing_file("Procfile.dev", stale_procfile) + simulate_base_webpack_files + simulate_existing_file("client/node-renderer.js", legacy_renderer_content) + allow(Gem).to receive(:loaded_specs).and_return({ "react_on_rails_pro" => double }) + + Dir.chdir(destination_root) do + run_generator(["--force"]) + end + end + + it "surfaces a pointed warning about the stale legacy Procfile line" do + expect(GeneratorMessages.messages.join("\n")) + .to include("Procfile.dev still launches the legacy client/node-renderer.js") + end + + it "leaves the stale legacy Procfile entry untouched" do + expect(File.read(File.join(destination_root, "Procfile.dev"))).to eq(stale_procfile) + end + end + + context "when legacy client/node-renderer.js exists and Procfile.dev only comments the legacy command" do + let(:legacy_renderer_content) { "// customized legacy renderer\n" } + let(:commented_legacy_procfile) do + "rails: bin/rails s\n" \ + "# node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node client/node-renderer.js\n" + end + + before do + prepare_destination + simulate_existing_rails_files(package_json: true) + simulate_existing_file("Gemfile", <<~RUBY) + source "https://rubygems.org" + gem "react_on_rails_pro" + RUBY + simulate_npm_files(package_json: true) + simulate_existing_file("config/initializers/react_on_rails.rb", "ReactOnRails.configure {}") + simulate_existing_file("Procfile.dev", commented_legacy_procfile) + simulate_base_webpack_files + simulate_existing_file("client/node-renderer.js", legacy_renderer_content) + allow(Gem).to receive(:loaded_specs).and_return({ "react_on_rails_pro" => double }) + + Dir.chdir(destination_root) do + run_generator(["--force"]) + end + end + + it "does not report a stale legacy entry warning for commented lines" do + expect(GeneratorMessages.messages.join("\n")) + .not_to include("Procfile.dev still launches the legacy client/node-renderer.js") + end + end + + context "when both renderer/node-renderer.js and legacy client/node-renderer.js exist" do + let(:existing_renderer_content) { "// existing renderer\n" } + let(:legacy_renderer_content) { "// customized legacy renderer\n" } + + before do + prepare_destination + simulate_existing_rails_files(package_json: true) + simulate_existing_file("Gemfile", <<~RUBY) + source "https://rubygems.org" + gem "react_on_rails_pro" + RUBY + simulate_npm_files(package_json: true) + simulate_existing_file("config/initializers/react_on_rails.rb", "ReactOnRails.configure {}") + simulate_existing_file("Procfile.dev", "rails: bin/rails s\n") + simulate_base_webpack_files + simulate_existing_file("renderer/node-renderer.js", existing_renderer_content) + simulate_existing_file("client/node-renderer.js", legacy_renderer_content) + allow(Gem).to receive(:loaded_specs).and_return({ "react_on_rails_pro" => double }) + + Dir.chdir(destination_root) do + run_generator(["--force"]) + end + end + + it "preserves the existing renderer/node-renderer.js" do + expect(File.read(File.join(destination_root, "renderer/node-renderer.js"))).to eq(existing_renderer_content) + end + + it "preserves the legacy client/node-renderer.js" do + expect(File.read(File.join(destination_root, "client/node-renderer.js"))).to eq(legacy_renderer_content) + end + + it "adds exactly one renderer/ node-renderer entry to Procfile.dev" do + procfile = File.read(File.join(destination_root, "Procfile.dev")) + expect(procfile) + .to include("node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js") + expect(procfile.scan(/^node-renderer:/).size).to eq(1) + end + end + context "when server webpack has only libraryTarget uncommented" do before do prepare_destination diff --git a/react_on_rails/spec/react_on_rails/support/shared_examples/pro_generator_examples.rb b/react_on_rails/spec/react_on_rails/support/shared_examples/pro_generator_examples.rb index 46c1323d30..c4042958b2 100644 --- a/react_on_rails/spec/react_on_rails/support/shared_examples/pro_generator_examples.rb +++ b/react_on_rails/spec/react_on_rails/support/shared_examples/pro_generator_examples.rb @@ -9,7 +9,7 @@ end it "creates node-renderer.js bootstrap file" do - assert_file "client/node-renderer.js" do |content| + assert_file "renderer/node-renderer.js" do |content| expect(content).to include("reactOnRailsProNodeRenderer") end end diff --git a/react_on_rails_pro/.controlplane/rails.yml b/react_on_rails_pro/.controlplane/rails.yml index 36aacf214e..79e5d6a0af 100644 --- a/react_on_rails_pro/.controlplane/rails.yml +++ b/react_on_rails_pro/.controlplane/rails.yml @@ -20,7 +20,7 @@ spec: protocol: http - name: node-renderer args: - - client/node-renderer.js + - renderer/node-renderer.js command: node cpu: 512m env: @@ -46,4 +46,4 @@ spec: - 0.0.0.0/0 # Could configure outbound for more security outboundAllowCIDR: - - 0.0.0.0/0 \ No newline at end of file + - 0.0.0.0/0 diff --git a/react_on_rails_pro/spec/dummy/Procfile.dev b/react_on_rails_pro/spec/dummy/Procfile.dev index dc69ec18e9..d6aebbda2d 100644 --- a/react_on_rails_pro/spec/dummy/Procfile.dev +++ b/react_on_rails_pro/spec/dummy/Procfile.dev @@ -11,4 +11,4 @@ rails-server-assets: HMR=true SERVER_BUNDLE_ONLY=true bin/shakapacker --watch rails-rsc-assets: HMR=true RSC_BUNDLE_ONLY=true bin/shakapacker --watch # Start Node server for server rendering. -node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node --inspect client/node-renderer.js +node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node --inspect renderer/node-renderer.js diff --git a/react_on_rails_pro/spec/dummy/Procfile.prod b/react_on_rails_pro/spec/dummy/Procfile.prod index d47e98ef15..93e2d85814 100644 --- a/react_on_rails_pro/spec/dummy/Procfile.prod +++ b/react_on_rails_pro/spec/dummy/Procfile.prod @@ -3,4 +3,4 @@ rails: RAILS_ENV=production NODE_ENV=production bin/rails s -p 3001 # Start Node server for server rendering. -node-renderer: NODE_ENV=production RENDERER_LOG_LEVEL=error RENDERER_PORT=3800 node client/node-renderer.js +node-renderer: NODE_ENV=production RENDERER_LOG_LEVEL=error RENDERER_PORT=3800 node renderer/node-renderer.js diff --git a/react_on_rails_pro/spec/dummy/package.json b/react_on_rails_pro/spec/dummy/package.json index daf599fa49..c0f97d36f2 100644 --- a/react_on_rails_pro/spec/dummy/package.json +++ b/react_on_rails_pro/spec/dummy/package.json @@ -104,8 +104,8 @@ "build:client": "RAILS_ENV=production NODE_ENV=production bin/shakapacker", "build:server": "RAILS_ENV=production NODE_ENV=production SERVER=true bin/shakapacker", "build:clean": "rm -rf public/webpack && rm -rf ssr-generated || true", - "node-renderer-debug": "RENDERER_PORT=3800 ndb client/node-renderer.js", - "node-renderer": "RENDERER_PORT=3800 node client/node-renderer.js" + "node-renderer-debug": "RENDERER_PORT=3800 ndb renderer/node-renderer.js", + "node-renderer": "RENDERER_PORT=3800 node renderer/node-renderer.js" }, "license": "UNLICENSED", "repository": { diff --git a/react_on_rails_pro/spec/dummy/client/node-renderer.js b/react_on_rails_pro/spec/dummy/renderer/node-renderer.js similarity index 100% rename from react_on_rails_pro/spec/dummy/client/node-renderer.js rename to react_on_rails_pro/spec/dummy/renderer/node-renderer.js