Skip to content

Commit 1022234

Browse files
justin808claude
andcommitted
Fix SSR runtime failures: Node renderer, polyfills, and RSC classification
Three root causes for the 37/38 rspec test failures: 1. CI missing Node renderer: The RSC branch switched SSR from ExecJS to the react-on-rails-pro NodeRenderer service (port 3800). CI never started this service, causing Net::ReadTimeout on all SSR requests. Added renderer startup step and RENDERER_PASSWORD env var to CI. 2. Server bundle externals broke in VM sandbox: The previous commit externalized Node builtins (path/fs/stream) as CommonJS requires, but the Node renderer runs bundles in a vm.createContext() sandbox where require() is unavailable. Reverted to resolve.fallback: false which stubs these unused code paths at build time instead. 3. MessageChannel undefined in VM: react-dom/server.browser.js instantiates MessageChannel at module load time. The Node renderer's VM sandbox lacks this browser global (unlike Bun/ExecJS on master). Added a BannerPlugin polyfill injected at bundle top. 4. RouterApp.server.jsx misclassified as RSC: The auto-bundling system registered it via registerServerComponent() because it lacked 'use client'. But it's a traditional SSR component (StaticRouter), not an RSC. Added 'use client' directive so it registers via ReactOnRails.register() instead. All 38 rspec tests now pass locally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3f7e452 commit 1022234

3 files changed

Lines changed: 47 additions & 17 deletions

File tree

.github/workflows/rspec_test.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
DRIVER: selenium_chrome
3434
CHROME_BIN: /usr/bin/google-chrome
3535
USE_COVERALLS: true
36+
RENDERER_PASSWORD: devPassword
3637

3738
steps:
3839
- name: Install Chrome
@@ -82,6 +83,20 @@ jobs:
8283
- name: Build shakapacker chunks
8384
run: NODE_ENV=development bundle exec bin/shakapacker
8485

86+
- name: Start Node renderer for SSR
87+
run: |
88+
node react-on-rails-pro-node-renderer.js &
89+
echo "Waiting for Node renderer on port 3800..."
90+
for i in $(seq 1 30); do
91+
if nc -z localhost 3800 2>/dev/null; then
92+
echo "Node renderer is ready"
93+
exit 0
94+
fi
95+
sleep 1
96+
done
97+
echo "Node renderer failed to start within 30 seconds"
98+
exit 1
99+
85100
- name: Run rspec with xvfb
86101
uses: coactions/setup-xvfb@v1
87102
with:

client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
// Compare to ./RouterApp.client.jsx
24
import { Provider } from 'react-redux';
35
import React from 'react';

config/webpack/serverWebpackConfig.js

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -162,26 +162,39 @@ const configureServer = () => {
162162
// The default of cheap-module-source-map is slow and provides poor info.
163163
serverWebpackConfig.devtool = 'eval';
164164

165-
// Alias react-dom/server to the Node.js version for the Pro Node renderer.
166-
// The default browser version uses MessageChannel which isn't available in the Node VM.
165+
// react-on-rails-pro includes RSC-related modules that import Node.js builtins
166+
// (path, fs, stream). These code paths aren't exercised in the SSR bundle,
167+
// so provide empty fallbacks to satisfy the resolver without bundling them.
167168
serverWebpackConfig.resolve = serverWebpackConfig.resolve || {};
168-
serverWebpackConfig.resolve.alias = {
169-
...serverWebpackConfig.resolve.alias,
170-
'react-dom/server.browser$': 'react-dom/server.node',
171-
'react-dom/server.browser.js$': 'react-dom/server.node.js',
169+
serverWebpackConfig.resolve.fallback = {
170+
...serverWebpackConfig.resolve.fallback,
171+
path: false,
172+
fs: false,
173+
'fs/promises': false,
174+
stream: false,
172175
};
173176

174-
// react-on-rails-pro includes RSC-related modules that import Node.js builtins
175-
// (path, fs, stream). Externalize them so they resolve at runtime via require()
176-
// in the Node.js environment where the SSR bundle executes.
177-
const existingExternals = serverWebpackConfig.externals || {};
178-
serverWebpackConfig.externals = {
179-
...(typeof existingExternals === 'object' && !Array.isArray(existingExternals) ? existingExternals : {}),
180-
path: 'commonjs path',
181-
fs: 'commonjs fs',
182-
'fs/promises': 'commonjs fs/promises',
183-
stream: 'commonjs stream',
184-
};
177+
// The Node renderer runs bundles in a VM sandbox that lacks browser globals
178+
// like MessageChannel and TextEncoder. Inject polyfills at the top of the
179+
// bundle so react-dom/server.browser can initialize.
180+
serverWebpackConfig.plugins.push(
181+
new bundler.BannerPlugin({
182+
banner: [
183+
'if(typeof MessageChannel==="undefined"){',
184+
' globalThis.MessageChannel=class MessageChannel{',
185+
' constructor(){',
186+
' this.port1={onmessage:null};',
187+
' this.port2={postMessage:function(msg){',
188+
' var p=this._port1;if(p.onmessage)p.onmessage({data:msg});',
189+
' }};',
190+
' this.port2._port1=this.port1;',
191+
' }',
192+
' };',
193+
'}',
194+
].join('\n'),
195+
raw: true,
196+
}),
197+
);
185198

186199
return serverWebpackConfig;
187200
};

0 commit comments

Comments
 (0)