react_on_rails 16.4.0.rc.1 → 16.4.0.rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/Gemfile.lock +1 -1
- data/lib/generators/react_on_rails/base_generator.rb +30 -10
- data/lib/generators/react_on_rails/dev_tests_generator.rb +9 -1
- data/lib/generators/react_on_rails/generator_helper.rb +101 -0
- data/lib/generators/react_on_rails/generator_messages.rb +2 -2
- data/lib/generators/react_on_rails/install_generator.rb +55 -19
- data/lib/generators/react_on_rails/js_dependency_manager.rb +115 -8
- data/lib/generators/react_on_rails/pro/USAGE +21 -0
- data/lib/generators/react_on_rails/pro_generator.rb +97 -0
- data/lib/generators/react_on_rails/pro_setup.rb +314 -0
- data/lib/generators/react_on_rails/rsc/USAGE +23 -0
- data/lib/generators/react_on_rails/rsc_generator.rb +100 -0
- data/lib/generators/react_on_rails/rsc_setup.rb +471 -0
- data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +3 -3
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/{generateWebpackConfigs.js.tt → ServerClientOrBoth.js.tt} +28 -3
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +8 -0
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +54 -0
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_server_spec.rb +17 -0
- data/lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js +41 -0
- data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +25 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/controllers/hello_server_controller.rb +25 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.jsx +80 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.tsx +90 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.jsx +54 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.tsx +54 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.jsx +10 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.tsx +10 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb +37 -0
- data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +64 -0
- data/lib/react_on_rails/engine.rb +5 -1
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails/version_checker.rb +6 -1
- metadata +21 -3
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/test.js") %>
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const serverClientOrBoth = require('./ServerClientOrBoth')
|
|
4
4
|
|
|
5
5
|
const testOnly = (_clientWebpackConfig, _serverWebpackConfig) => {
|
|
6
6
|
// place any code here that is for test only
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
module.exports =
|
|
9
|
+
module.exports = serverClientOrBoth(testOnly)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rails_helper"
|
|
4
|
+
|
|
5
|
+
describe "Hello Server", :js do
|
|
6
|
+
it "renders the React Server Component" do
|
|
7
|
+
visit "/hello_server"
|
|
8
|
+
expect(page).to have_text("Hello, React on Rails Pro!")
|
|
9
|
+
expect(page).to have_text("How is this different from SSR?")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "renders the interactive LikeButton client component" do
|
|
13
|
+
visit "/hello_server"
|
|
14
|
+
expect(page).to have_button("👍 Like")
|
|
15
|
+
expect(page).to have_text("0 likes")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { reactOnRailsProNodeRenderer } = require('react-on-rails-pro-node-renderer');
|
|
3
|
+
|
|
4
|
+
const { env } = process;
|
|
5
|
+
|
|
6
|
+
const config = {
|
|
7
|
+
serverBundleCachePath: path.resolve(__dirname, '../.node-renderer-bundles'),
|
|
8
|
+
port: Number(env.RENDERER_PORT) || 3800,
|
|
9
|
+
logLevel: env.RENDERER_LOG_LEVEL || 'info',
|
|
10
|
+
|
|
11
|
+
// See value in /config/initializers/react_on_rails_pro.rb
|
|
12
|
+
password: env.RENDERER_PASSWORD || 'devPassword',
|
|
13
|
+
|
|
14
|
+
// Number of Node.js worker threads for SSR rendering
|
|
15
|
+
// Set NODE_RENDERER_CONCURRENCY env var to override (e.g., for production tuning)
|
|
16
|
+
workersCount: env.NODE_RENDERER_CONCURRENCY != null ? Number(env.NODE_RENDERER_CONCURRENCY) : 3,
|
|
17
|
+
|
|
18
|
+
// If set to true, `supportModules` enables the server-bundle code to call a default set of NodeJS modules
|
|
19
|
+
// that get added to the VM context: { Buffer, process, setTimeout, setInterval, clearTimeout, clearInterval }.
|
|
20
|
+
// This option is required to equal `true` if you want to use loadable components.
|
|
21
|
+
// Setting this value to false causes the NodeRenderer to behave like ExecJS
|
|
22
|
+
supportModules: true,
|
|
23
|
+
|
|
24
|
+
// Additional Node.js globals to add to the VM context.
|
|
25
|
+
additionalContext: { URL, AbortController },
|
|
26
|
+
|
|
27
|
+
// Required to use setTimeout, setInterval, & clearTimeout during server rendering
|
|
28
|
+
stubTimers: false,
|
|
29
|
+
|
|
30
|
+
// Replay console logs from async server operations
|
|
31
|
+
replayServerAsyncOperationLogs: true,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Renderer detects a total number of CPUs on virtual hostings like Heroku or CircleCI instead
|
|
35
|
+
// of CPUs number allocated for current container. This results in spawning many workers while
|
|
36
|
+
// only 1-2 of them really needed.
|
|
37
|
+
if (env.CI) {
|
|
38
|
+
config.workersCount = 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
reactOnRailsProNodeRenderer(config);
|
data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# See https://www.shakacode.com/react-on-rails-pro/docs/configuration/
|
|
4
|
+
# License: Set REACT_ON_RAILS_PRO_LICENSE environment variable
|
|
5
|
+
ReactOnRailsPro.configure do |config|
|
|
6
|
+
config.server_renderer = "NodeRenderer"
|
|
7
|
+
config.renderer_url = ENV.fetch("REACT_RENDERER_URL", "http://localhost:3800")
|
|
8
|
+
|
|
9
|
+
# See value in client/node-renderer.js
|
|
10
|
+
config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
|
|
11
|
+
|
|
12
|
+
config.ssr_timeout = 5
|
|
13
|
+
config.renderer_request_retry_limit = 1
|
|
14
|
+
config.renderer_use_fallback_exec_js = Rails.env.development?
|
|
15
|
+
|
|
16
|
+
# If you want Honeybadger or Sentry on the Node renderer side to report rendering errors
|
|
17
|
+
config.throw_js_errors = false
|
|
18
|
+
|
|
19
|
+
# If true, then cache the evaluation of JS for prerendering using the standard Rails cache.
|
|
20
|
+
# Applies to all rendering engines.
|
|
21
|
+
config.prerender_caching = true
|
|
22
|
+
|
|
23
|
+
# Get timing of server render calls
|
|
24
|
+
config.tracing = Rails.env.development?
|
|
25
|
+
end
|
data/lib/generators/react_on_rails/templates/rsc/base/app/controllers/hello_server_controller.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# HelloServer Controller - React Server Components
|
|
4
|
+
# This controller demonstrates how to render RSC pages with streaming SSR.
|
|
5
|
+
# It's the RSC counterpart to HelloWorldController.
|
|
6
|
+
#
|
|
7
|
+
# ReactOnRailsPro::Stream provides:
|
|
8
|
+
# - stream_view_containing_react_components: Streams the view with RSC support
|
|
9
|
+
# - Streaming HTML chunks as components render
|
|
10
|
+
# - Automatic hydration on the client
|
|
11
|
+
#
|
|
12
|
+
# For more information, see:
|
|
13
|
+
# https://www.shakacode.com/react-on-rails-pro/docs/react-server-components/
|
|
14
|
+
|
|
15
|
+
class HelloServerController < ApplicationController
|
|
16
|
+
include ReactOnRailsPro::Stream
|
|
17
|
+
|
|
18
|
+
def index
|
|
19
|
+
@hello_server_props = {
|
|
20
|
+
name: "React on Rails Pro"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
stream_view_containing_react_components(template: "hello_server/index")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// HelloServer - React Server Component
|
|
2
|
+
//
|
|
3
|
+
// This component runs ONLY on the server. No JavaScript for it is sent to the browser.
|
|
4
|
+
// It demonstrates key RSC capabilities:
|
|
5
|
+
//
|
|
6
|
+
// 1. Async data fetching — use await directly in the component (no useEffect needed)
|
|
7
|
+
// 2. Server-only code — access databases, file systems, or secrets safely
|
|
8
|
+
// 3. Zero client JS — heavy libraries used here don't increase your bundle size
|
|
9
|
+
// 4. Streaming — wrapped in <Suspense>, this component streams HTML as it resolves
|
|
10
|
+
//
|
|
11
|
+
// For more information, see:
|
|
12
|
+
// https://www.shakacode.com/react-on-rails-pro/docs/react-server-components/
|
|
13
|
+
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import LikeButton from './LikeButton';
|
|
16
|
+
|
|
17
|
+
// Simulate an async data fetch (replace with a real API call or DB query)
|
|
18
|
+
async function fetchGreeting(name) {
|
|
19
|
+
// In a real app, you could do:
|
|
20
|
+
// const data = await db.query('SELECT greeting FROM greetings WHERE name = ?', [name]);
|
|
21
|
+
// const file = await fs.readFile('./data/greeting.txt', 'utf-8');
|
|
22
|
+
// const res = await fetch('http://internal-api/greeting');
|
|
23
|
+
// eslint-disable-next-line no-promise-executor-return
|
|
24
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
25
|
+
|
|
26
|
+
const now = new Date();
|
|
27
|
+
return {
|
|
28
|
+
message: `Hello, ${name}!`,
|
|
29
|
+
serverTime: now.toLocaleString('en-US', {
|
|
30
|
+
weekday: 'long',
|
|
31
|
+
year: 'numeric',
|
|
32
|
+
month: 'long',
|
|
33
|
+
day: 'numeric',
|
|
34
|
+
hour: '2-digit',
|
|
35
|
+
minute: '2-digit',
|
|
36
|
+
second: '2-digit',
|
|
37
|
+
timeZoneName: 'short',
|
|
38
|
+
}),
|
|
39
|
+
facts: [
|
|
40
|
+
'This component rendered entirely on the server — zero JS was sent to the browser for it.',
|
|
41
|
+
'The date formatting above used server-side Intl APIs — no date library shipped to the client.',
|
|
42
|
+
'The "Like" button below IS a client component — only its JS is sent to the browser.',
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const HelloServer = async ({ name = 'World' }) => {
|
|
48
|
+
const { message, serverTime, facts } = await fetchGreeting(name);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 600, margin: '0 auto' }}>
|
|
52
|
+
<h2>{message}</h2>
|
|
53
|
+
<p style={{ color: '#666', fontSize: '0.9em' }}>Server-rendered at: {serverTime}</p>
|
|
54
|
+
|
|
55
|
+
<div
|
|
56
|
+
style={{
|
|
57
|
+
background: '#f0f9ff',
|
|
58
|
+
border: '1px solid #bae6fd',
|
|
59
|
+
borderRadius: 8,
|
|
60
|
+
padding: 16,
|
|
61
|
+
margin: '16px 0',
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<h3 style={{ margin: '0 0 12px' }}>How is this different from SSR?</h3>
|
|
65
|
+
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
|
66
|
+
{facts.map((fact) => (
|
|
67
|
+
<li key={fact} style={{ marginBottom: 8 }}>
|
|
68
|
+
{fact}
|
|
69
|
+
</li>
|
|
70
|
+
))}
|
|
71
|
+
</ul>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* LikeButton is a client component — it ships JS for interactivity */}
|
|
75
|
+
<LikeButton />
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default HelloServer;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// HelloServer - React Server Component
|
|
2
|
+
//
|
|
3
|
+
// This component runs ONLY on the server. No JavaScript for it is sent to the browser.
|
|
4
|
+
// It demonstrates key RSC capabilities:
|
|
5
|
+
//
|
|
6
|
+
// 1. Async data fetching — use await directly in the component (no useEffect needed)
|
|
7
|
+
// 2. Server-only code — access databases, file systems, or secrets safely
|
|
8
|
+
// 3. Zero client JS — heavy libraries used here don't increase your bundle size
|
|
9
|
+
// 4. Streaming — wrapped in <Suspense>, this component streams HTML as it resolves
|
|
10
|
+
//
|
|
11
|
+
// For more information, see:
|
|
12
|
+
// https://www.shakacode.com/react-on-rails-pro/docs/react-server-components/
|
|
13
|
+
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import LikeButton from './LikeButton';
|
|
16
|
+
|
|
17
|
+
interface HelloServerProps {
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface GreetingData {
|
|
22
|
+
message: string;
|
|
23
|
+
serverTime: string;
|
|
24
|
+
facts: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Simulate an async data fetch (replace with a real API call or DB query)
|
|
28
|
+
async function fetchGreeting(name: string): Promise<GreetingData> {
|
|
29
|
+
// In a real app, you could do:
|
|
30
|
+
// const data = await db.query('SELECT greeting FROM greetings WHERE name = ?', [name]);
|
|
31
|
+
// const file = await fs.readFile('./data/greeting.txt', 'utf-8');
|
|
32
|
+
// const res = await fetch('http://internal-api/greeting');
|
|
33
|
+
// eslint-disable-next-line no-promise-executor-return
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
35
|
+
|
|
36
|
+
const now = new Date();
|
|
37
|
+
return {
|
|
38
|
+
message: `Hello, ${name}!`,
|
|
39
|
+
serverTime: now.toLocaleString('en-US', {
|
|
40
|
+
weekday: 'long',
|
|
41
|
+
year: 'numeric',
|
|
42
|
+
month: 'long',
|
|
43
|
+
day: 'numeric',
|
|
44
|
+
hour: '2-digit',
|
|
45
|
+
minute: '2-digit',
|
|
46
|
+
second: '2-digit',
|
|
47
|
+
timeZoneName: 'short',
|
|
48
|
+
}),
|
|
49
|
+
facts: [
|
|
50
|
+
'This component rendered entirely on the server — zero JS was sent to the browser for it.',
|
|
51
|
+
'The date formatting above used server-side Intl APIs — no date library shipped to the client.',
|
|
52
|
+
'The "Like" button below IS a client component — only its JS is sent to the browser.',
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const HelloServer = async ({ name = 'World' }: HelloServerProps) => {
|
|
58
|
+
const { message, serverTime, facts } = await fetchGreeting(name);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 600, margin: '0 auto' }}>
|
|
62
|
+
<h2>{message}</h2>
|
|
63
|
+
<p style={{ color: '#666', fontSize: '0.9em' }}>Server-rendered at: {serverTime}</p>
|
|
64
|
+
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
background: '#f0f9ff',
|
|
68
|
+
border: '1px solid #bae6fd',
|
|
69
|
+
borderRadius: 8,
|
|
70
|
+
padding: 16,
|
|
71
|
+
margin: '16px 0',
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<h3 style={{ margin: '0 0 12px' }}>How is this different from SSR?</h3>
|
|
75
|
+
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
|
76
|
+
{facts.map((fact) => (
|
|
77
|
+
<li key={fact} style={{ marginBottom: 8 }}>
|
|
78
|
+
{fact}
|
|
79
|
+
</li>
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* LikeButton is a client component — it ships JS for interactivity */}
|
|
85
|
+
<LikeButton />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default HelloServer;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// LikeButton - Client Component
|
|
4
|
+
//
|
|
5
|
+
// This component has the 'use client' directive, so its JavaScript IS sent to the browser.
|
|
6
|
+
// It demonstrates how client components work alongside server components:
|
|
7
|
+
//
|
|
8
|
+
// - Only this component's JS is included in the client bundle
|
|
9
|
+
// - The parent HelloServer component sends ZERO JS to the browser
|
|
10
|
+
// - React hydrates just this interactive island within the server-rendered HTML
|
|
11
|
+
|
|
12
|
+
import React, { useState } from 'react';
|
|
13
|
+
|
|
14
|
+
const LikeButton = () => {
|
|
15
|
+
const [likes, setLikes] = useState(0);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
style={{
|
|
20
|
+
display: 'flex',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
gap: 12,
|
|
23
|
+
marginTop: 16,
|
|
24
|
+
padding: '12px 16px',
|
|
25
|
+
background: '#fefce8',
|
|
26
|
+
border: '1px solid #fde68a',
|
|
27
|
+
borderRadius: 8,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={() => setLikes((prev) => prev + 1)}
|
|
33
|
+
style={{
|
|
34
|
+
padding: '8px 16px',
|
|
35
|
+
fontSize: '1em',
|
|
36
|
+
cursor: 'pointer',
|
|
37
|
+
borderRadius: 6,
|
|
38
|
+
border: '1px solid #d1d5db',
|
|
39
|
+
background: '#fff',
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
👍 Like
|
|
43
|
+
</button>
|
|
44
|
+
<span>
|
|
45
|
+
{likes} {likes === 1 ? 'like' : 'likes'}
|
|
46
|
+
</span>
|
|
47
|
+
<span style={{ color: '#92400e', fontSize: '0.85em' }}>
|
|
48
|
+
← This button is a client component (check your browser's Network tab — only its JS was sent)
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default LikeButton;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// LikeButton - Client Component
|
|
4
|
+
//
|
|
5
|
+
// This component has the 'use client' directive, so its JavaScript IS sent to the browser.
|
|
6
|
+
// It demonstrates how client components work alongside server components:
|
|
7
|
+
//
|
|
8
|
+
// - Only this component's JS is included in the client bundle
|
|
9
|
+
// - The parent HelloServer component sends ZERO JS to the browser
|
|
10
|
+
// - React hydrates just this interactive island within the server-rendered HTML
|
|
11
|
+
|
|
12
|
+
import React, { useState } from 'react';
|
|
13
|
+
|
|
14
|
+
const LikeButton: React.FC = () => {
|
|
15
|
+
const [likes, setLikes] = useState(0);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
style={{
|
|
20
|
+
display: 'flex',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
gap: 12,
|
|
23
|
+
marginTop: 16,
|
|
24
|
+
padding: '12px 16px',
|
|
25
|
+
background: '#fefce8',
|
|
26
|
+
border: '1px solid #fde68a',
|
|
27
|
+
borderRadius: 8,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={() => setLikes((prev) => prev + 1)}
|
|
33
|
+
style={{
|
|
34
|
+
padding: '8px 16px',
|
|
35
|
+
fontSize: '1em',
|
|
36
|
+
cursor: 'pointer',
|
|
37
|
+
borderRadius: 6,
|
|
38
|
+
border: '1px solid #d1d5db',
|
|
39
|
+
background: '#fff',
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
👍 Like
|
|
43
|
+
</button>
|
|
44
|
+
<span>
|
|
45
|
+
{likes} {likes === 1 ? 'like' : 'likes'}
|
|
46
|
+
</span>
|
|
47
|
+
<span style={{ color: '#92400e', fontSize: '0.85em' }}>
|
|
48
|
+
← This button is a client component (check your browser's Network tab — only its JS was sent)
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default LikeButton;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// HelloServer Entry Point - Re-exports the server component for auto_load_bundle
|
|
2
|
+
// This file is discovered by React on Rails' auto_load_bundle feature
|
|
3
|
+
// and registered automatically in both client and server bundles.
|
|
4
|
+
//
|
|
5
|
+
// Note: No 'use client' directive here - this is a Server Component.
|
|
6
|
+
// The actual component in ../components/HelloServer.jsx runs only on the server.
|
|
7
|
+
|
|
8
|
+
import HelloServer from '../components/HelloServer';
|
|
9
|
+
|
|
10
|
+
export default HelloServer;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// HelloServer Entry Point - Re-exports the server component for auto_load_bundle
|
|
2
|
+
// This file is discovered by React on Rails' auto_load_bundle feature
|
|
3
|
+
// and registered automatically in both client and server bundles.
|
|
4
|
+
//
|
|
5
|
+
// Note: No 'use client' directive here - this is a Server Component.
|
|
6
|
+
// The actual component in ../components/HelloServer.tsx runs only on the server.
|
|
7
|
+
|
|
8
|
+
import HelloServer from '../components/HelloServer';
|
|
9
|
+
|
|
10
|
+
export default HelloServer;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<h1>React Server Components Demo</h1>
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
This page demonstrates <strong>React Server Components (RSC)</strong> with React on Rails Pro.
|
|
5
|
+
The component below is an <em>async server component</em> that fetches data on the server
|
|
6
|
+
and streams HTML to the browser. No component JavaScript is sent to the client.
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<%#
|
|
10
|
+
stream_react_component renders the RSC with streaming support.
|
|
11
|
+
|
|
12
|
+
Key points:
|
|
13
|
+
- prerender: true enables server-side rendering with streaming
|
|
14
|
+
- Props are passed from the controller (@hello_server_props)
|
|
15
|
+
- The component streams as it resolves (async data fetching happens server-side)
|
|
16
|
+
- Only the interactive LikeButton client component ships JS to the browser
|
|
17
|
+
- Open DevTools Network tab to verify — no HelloServer.js in the client bundle
|
|
18
|
+
%>
|
|
19
|
+
<%= stream_react_component('HelloServer', props: @hello_server_props, prerender: true) %>
|
|
20
|
+
|
|
21
|
+
<hr>
|
|
22
|
+
|
|
23
|
+
<details style="margin-top: 16px;">
|
|
24
|
+
<summary><strong>What makes this different from traditional SSR?</strong></summary>
|
|
25
|
+
<ul style="margin-top: 8px;">
|
|
26
|
+
<li><strong>Traditional SSR:</strong> Server renders HTML, then ships ALL component JS to the browser for hydration. Every library used in rendering is in your bundle.</li>
|
|
27
|
+
<li><strong>RSC:</strong> Server components stay on the server permanently. Only client components (marked with <code>'use client'</code>) ship JS. Libraries used in server components are never bundled for the client.</li>
|
|
28
|
+
<li><strong>Async by default:</strong> Server components can be async functions that <code>await</code> data directly — no <code>useEffect</code>, no loading states for initial render, no client-side fetch waterfalls.</li>
|
|
29
|
+
</ul>
|
|
30
|
+
</details>
|
|
31
|
+
|
|
32
|
+
<p style="margin-top: 16px;">
|
|
33
|
+
<strong>Learn more:</strong>
|
|
34
|
+
<a href="https://www.shakacode.com/react-on-rails-pro/docs/react-server-components/">
|
|
35
|
+
React Server Components Documentation
|
|
36
|
+
</a>
|
|
37
|
+
</p>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// React Server Components webpack configuration
|
|
2
|
+
// This creates the RSC bundle based on the server webpack config
|
|
3
|
+
// See: https://www.shakacode.com/react-on-rails-pro/docs/react-server-components/
|
|
4
|
+
|
|
5
|
+
const serverWebpackModule = require('./serverWebpackConfig');
|
|
6
|
+
|
|
7
|
+
// Backward compatibility:
|
|
8
|
+
// - New Pro config exports: { default: configureServer, extractLoader }
|
|
9
|
+
// - Legacy config exports: module.exports = configureServer
|
|
10
|
+
const serverWebpackConfig = serverWebpackModule.default || serverWebpackModule;
|
|
11
|
+
const extractLoader = serverWebpackModule.extractLoader || ((rule, loaderName) => {
|
|
12
|
+
if (!Array.isArray(rule.use)) return null;
|
|
13
|
+
return rule.use.find((item) => {
|
|
14
|
+
const testValue = typeof item === 'string' ? item : item.loader;
|
|
15
|
+
return testValue && testValue.includes(loaderName);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const configureRsc = () => {
|
|
20
|
+
// Pass true to skip RSCWebpackPlugin - RSC bundle doesn't need it
|
|
21
|
+
const rscConfig = serverWebpackConfig(true);
|
|
22
|
+
|
|
23
|
+
// Update the entry name to be `rsc-bundle` instead of `server-bundle`
|
|
24
|
+
const rscEntry = {
|
|
25
|
+
'rsc-bundle': rscConfig.entry['server-bundle'],
|
|
26
|
+
};
|
|
27
|
+
rscConfig.entry = rscEntry;
|
|
28
|
+
|
|
29
|
+
// Add the RSC loader before the JS loader (babel-loader or swc-loader)
|
|
30
|
+
// This loader properly excludes client components from the RSC bundle
|
|
31
|
+
const { rules } = rscConfig.module;
|
|
32
|
+
rules.forEach((rule) => {
|
|
33
|
+
if (Array.isArray(rule.use)) {
|
|
34
|
+
// Find the JS loader (could be babel-loader or swc-loader depending on configuration)
|
|
35
|
+
const jsLoader = extractLoader(rule, 'babel-loader') || extractLoader(rule, 'swc-loader');
|
|
36
|
+
if (jsLoader) {
|
|
37
|
+
rule.use.push({
|
|
38
|
+
loader: 'react-on-rails-rsc/WebpackLoader',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Add the `react-server` condition to the resolve config
|
|
45
|
+
// This condition is used by React and React on Rails to identify RSC bundles
|
|
46
|
+
// The `...` tells webpack to retain the default conditions (e.g., `node` for server target)
|
|
47
|
+
rscConfig.resolve = {
|
|
48
|
+
...rscConfig.resolve,
|
|
49
|
+
conditionNames: ['react-server', '...'],
|
|
50
|
+
alias: {
|
|
51
|
+
...rscConfig.resolve?.alias,
|
|
52
|
+
// Ignore react-dom/server in RSC bundle - it's not needed for RSC payload generation
|
|
53
|
+
// Not removing it will cause a runtime error
|
|
54
|
+
'react-dom/server': false,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Update the output bundle name to be `rsc-bundle.js` instead of `server-bundle.js`
|
|
59
|
+
rscConfig.output.filename = 'rsc-bundle.js';
|
|
60
|
+
|
|
61
|
+
return rscConfig;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = configureRsc;
|
|
@@ -69,8 +69,12 @@ module ReactOnRails
|
|
|
69
69
|
|
|
70
70
|
# Check if we're running a Rails generator
|
|
71
71
|
# @return [Boolean] true if running a generator
|
|
72
|
+
#
|
|
73
|
+
# Heuristic: Rails::Generators is typically only defined during generator
|
|
74
|
+
# commands. It could be defined by test helpers or gems that require
|
|
75
|
+
# "rails/generators", but this is a fallback behind the ENV check above.
|
|
72
76
|
def self.running_generator?
|
|
73
|
-
|
|
77
|
+
defined?(Rails::Generators)
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
# Check if package.json doesn't exist yet
|
|
@@ -121,7 +121,12 @@ module ReactOnRails
|
|
|
121
121
|
|
|
122
122
|
When using React on Rails Pro, you must use the 'react-on-rails-pro' npm package.
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
If you're upgrading from React on Rails to Pro, run the generator with validation skipped:
|
|
125
|
+
REACT_ON_RAILS_SKIP_VALIDATION=true rails g react_on_rails:install --pro
|
|
126
|
+
|
|
127
|
+
The generator will automatically replace the base package with the Pro package.
|
|
128
|
+
|
|
129
|
+
Or fix manually:
|
|
125
130
|
1. Remove the base package: #{remove_cmd}
|
|
126
131
|
2. Install the Pro package: #{install_cmd}
|
|
127
132
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: react_on_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 16.4.0.rc.
|
|
4
|
+
version: 16.4.0.rc.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Gordon
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: addressable
|
|
@@ -131,8 +131,14 @@ files:
|
|
|
131
131
|
- lib/generators/react_on_rails/generator_messages.rb
|
|
132
132
|
- lib/generators/react_on_rails/install_generator.rb
|
|
133
133
|
- lib/generators/react_on_rails/js_dependency_manager.rb
|
|
134
|
+
- lib/generators/react_on_rails/pro/USAGE
|
|
135
|
+
- lib/generators/react_on_rails/pro_generator.rb
|
|
136
|
+
- lib/generators/react_on_rails/pro_setup.rb
|
|
134
137
|
- lib/generators/react_on_rails/react_no_redux_generator.rb
|
|
135
138
|
- lib/generators/react_on_rails/react_with_redux_generator.rb
|
|
139
|
+
- lib/generators/react_on_rails/rsc/USAGE
|
|
140
|
+
- lib/generators/react_on_rails/rsc_generator.rb
|
|
141
|
+
- lib/generators/react_on_rails/rsc_setup.rb
|
|
136
142
|
- lib/generators/react_on_rails/templates/.eslintrc
|
|
137
143
|
- lib/generators/react_on_rails/templates/base/base/.dev-services.yml.example
|
|
138
144
|
- lib/generators/react_on_rails/templates/base/base/Procfile.dev
|
|
@@ -156,10 +162,10 @@ files:
|
|
|
156
162
|
- lib/generators/react_on_rails/templates/base/base/bin/switch-bundler
|
|
157
163
|
- lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt
|
|
158
164
|
- lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml.tt
|
|
165
|
+
- lib/generators/react_on_rails/templates/base/base/config/webpack/ServerClientOrBoth.js.tt
|
|
159
166
|
- lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt
|
|
160
167
|
- lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt
|
|
161
168
|
- lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt
|
|
162
|
-
- lib/generators/react_on_rails/templates/base/base/config/webpack/generateWebpackConfigs.js.tt
|
|
163
169
|
- lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt
|
|
164
170
|
- lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt
|
|
165
171
|
- lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt
|
|
@@ -169,7 +175,10 @@ files:
|
|
|
169
175
|
- lib/generators/react_on_rails/templates/dev_tests/spec/rails_helper.rb
|
|
170
176
|
- lib/generators/react_on_rails/templates/dev_tests/spec/simplecov_helper.rb
|
|
171
177
|
- lib/generators/react_on_rails/templates/dev_tests/spec/spec_helper.rb
|
|
178
|
+
- lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_server_spec.rb
|
|
172
179
|
- lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb
|
|
180
|
+
- lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js
|
|
181
|
+
- lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt
|
|
173
182
|
- lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.js
|
|
174
183
|
- lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts
|
|
175
184
|
- lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx
|
|
@@ -187,6 +196,15 @@ files:
|
|
|
187
196
|
- lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx
|
|
188
197
|
- lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.js
|
|
189
198
|
- lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts
|
|
199
|
+
- lib/generators/react_on_rails/templates/rsc/base/app/controllers/hello_server_controller.rb
|
|
200
|
+
- lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.jsx
|
|
201
|
+
- lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.tsx
|
|
202
|
+
- lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.jsx
|
|
203
|
+
- lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.tsx
|
|
204
|
+
- lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.jsx
|
|
205
|
+
- lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.tsx
|
|
206
|
+
- lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb
|
|
207
|
+
- lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt
|
|
190
208
|
- lib/react_on_rails.rb
|
|
191
209
|
- lib/react_on_rails/configuration.rb
|
|
192
210
|
- lib/react_on_rails/controller.rb
|