react_on_rails 17.0.0.rc.2 → 17.0.0.rc.4
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/Gemfile.lock +1 -1
- data/lib/generators/USAGE +6 -0
- data/lib/generators/react_on_rails/base_generator.rb +20 -2
- data/lib/generators/react_on_rails/dev_tests_generator.rb +20 -1
- data/lib/generators/react_on_rails/generator_helper.rb +7 -0
- data/lib/generators/react_on_rails/install_generator.rb +58 -1
- data/lib/generators/react_on_rails/js_dependency_manager.rb +37 -0
- data/lib/generators/react_on_rails/react_no_redux_generator.rb +20 -8
- data/lib/generators/react_on_rails/react_with_redux_generator.rb +28 -4
- data/lib/generators/react_on_rails/rsc_generator.rb +5 -0
- data/lib/generators/react_on_rails/rsc_setup.rb +35 -1
- data/lib/generators/react_on_rails/templates/agent_files/.cursor/rules/react-on-rails.mdc +11 -0
- data/lib/generators/react_on_rails/templates/agent_files/.github/copilot-instructions.md +7 -0
- data/lib/generators/react_on_rails/templates/agent_files/AGENTS.md +165 -0
- data/lib/generators/react_on_rails/templates/agent_files/CLAUDE.md +8 -0
- data/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler +6 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +81 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +30 -0
- data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +34 -0
- data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/stylesheets/application.css +1 -0
- data/lib/generators/react_on_rails/templates/dev_tests/eslint.config.mjs +50 -0
- data/lib/generators/react_on_rails/templates/redux/tailwind/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +25 -0
- data/lib/generators/react_on_rails/templates/redux/tailwind/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +29 -0
- data/lib/react_on_rails/controller/form_responders.rb +59 -0
- data/lib/react_on_rails/dev/server_manager.rb +1 -1
- data/lib/react_on_rails/doctor.rb +187 -24
- data/lib/react_on_rails/font_helper.rb +162 -0
- data/lib/react_on_rails/helper.rb +150 -0
- data/lib/react_on_rails/smart_error.rb +135 -5
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails.rb +1 -0
- data/lib/tasks/doctor.rake +5 -2
- data/sig/react_on_rails/controller/form_responders.rbs +10 -0
- data/sig/react_on_rails/helper.rbs +3 -0
- data/sig/react_on_rails/smart_error.rbs +25 -2
- metadata +14 -2
- data/lib/generators/react_on_rails/templates/dev_tests/.eslintrc +0 -25
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# AGENTS.md — React on Rails (for AI coding agents working IN this app)
|
|
2
|
+
|
|
3
|
+
This app uses **React on Rails** to render React components from Rails views, with
|
|
4
|
+
optional server-side rendering (SSR). This file teaches an AI agent the conventions
|
|
5
|
+
it needs to add, register, and render a component correctly on the first try.
|
|
6
|
+
|
|
7
|
+
It is intentionally short. For the full reference, see the links at the bottom.
|
|
8
|
+
|
|
9
|
+
> This file describes an app that **uses** React on Rails. It is not the
|
|
10
|
+
> contributor guide for the React on Rails framework itself.
|
|
11
|
+
|
|
12
|
+
## 1. Adding a component (auto-bundling — the default)
|
|
13
|
+
|
|
14
|
+
Place a component under any directory named `ror_components`. React on Rails
|
|
15
|
+
auto-bundles it, so **no manual registration is needed**:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
app/javascript/src/<Name>/ror_components/<Name>.tsx # or .jsx
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The component file **must `export default`** the React component. Example:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// app/javascript/src/SimpleCounter/ror_components/SimpleCounter.tsx
|
|
25
|
+
import React, { useState } from 'react';
|
|
26
|
+
|
|
27
|
+
const SimpleCounter = (props: { initialCount?: number }) => {
|
|
28
|
+
const [count, setCount] = useState(props.initialCount ?? 0);
|
|
29
|
+
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default SimpleCounter; // default export is required
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Manual registration (alternative)
|
|
36
|
+
|
|
37
|
+
If you are not using the `ror_components/` auto-bundling convention, register the
|
|
38
|
+
component explicitly from a pack/entry file, importing from the `react-on-rails`
|
|
39
|
+
npm package:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import ReactOnRails from 'react-on-rails';
|
|
43
|
+
import SimpleCounter from './SimpleCounter';
|
|
44
|
+
|
|
45
|
+
ReactOnRails.register({ SimpleCounter });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The key (`SimpleCounter`) is the name you pass to `react_component` in the Rails view.
|
|
49
|
+
|
|
50
|
+
## 2. Rendering it from a Rails view
|
|
51
|
+
|
|
52
|
+
Use the `react_component` view helper in any `.html.erb` view (e.g.
|
|
53
|
+
`app/views/<controller>/<action>.html.erb`). The first argument is the registered
|
|
54
|
+
component name; it must match the file/registration name exactly (case-sensitive).
|
|
55
|
+
|
|
56
|
+
```erb
|
|
57
|
+
<%= react_component("SimpleCounter", props: { initialCount: 5 }, prerender: true) %>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `props:` — a Ruby Hash serialized to JSON and passed to the component.
|
|
61
|
+
- `prerender:` — `true` renders the component on the server (SSR) then hydrates on
|
|
62
|
+
the client; `false` renders only on the client. Set `prerender: false` to isolate
|
|
63
|
+
SSR issues while debugging.
|
|
64
|
+
|
|
65
|
+
## 3. `.client` vs `.server` (and plain) bundles — what runs where
|
|
66
|
+
|
|
67
|
+
When a component name resolves to multiple files, React on Rails picks by suffix:
|
|
68
|
+
|
|
69
|
+
- `<Name>.client.tsx` — runs in the **browser** only. May use `window`, `document`,
|
|
70
|
+
browser-only APIs, and client-side hooks/effects.
|
|
71
|
+
- `<Name>.server.tsx` — runs during **server-side rendering** (Node, no DOM). It
|
|
72
|
+
**must not** use browser-only globals (`window`, `document`, `localStorage`).
|
|
73
|
+
Often it just re-exports the client component for SSR.
|
|
74
|
+
- `<Name>.tsx` (plain, no suffix) — used for **both** server and client. Keep it
|
|
75
|
+
isomorphic: guard any browser-only code with `if (typeof window !== 'undefined')`.
|
|
76
|
+
|
|
77
|
+
Rule of thumb: if `prerender: true`, the code path must run in Node without a DOM.
|
|
78
|
+
|
|
79
|
+
## 4. The `ReactOnRails` JS API you will actually touch
|
|
80
|
+
|
|
81
|
+
Import from the `react-on-rails` npm package:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import ReactOnRails from 'react-on-rails';
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `ReactOnRails.register({ Name })` — register one or more components by name.
|
|
88
|
+
- `ReactOnRails.registerStore({ name })` / `ReactOnRails.getStore(name)` — register
|
|
89
|
+
and retrieve a Redux store (only if this app uses Redux).
|
|
90
|
+
- `ReactOnRails.reactOnRailsPageLoaded()` — rarely needed; React on Rails wires
|
|
91
|
+
client hydration automatically.
|
|
92
|
+
|
|
93
|
+
You usually only need `register` (and only when not using `ror_components/`).
|
|
94
|
+
|
|
95
|
+
## 5. Top errors and fixes
|
|
96
|
+
|
|
97
|
+
These messages come straight from React on Rails' runtime errors. When you hit one,
|
|
98
|
+
apply the matching fix.
|
|
99
|
+
|
|
100
|
+
### `Component '<Name>' Not Registered`
|
|
101
|
+
|
|
102
|
+
The component name in the view does not match a registered/auto-bundled component.
|
|
103
|
+
|
|
104
|
+
- Recommended (auto-bundling): put the component file directly inside a
|
|
105
|
+
`ror_components/` directory, e.g.
|
|
106
|
+
`app/javascript/src/<Name>/ror_components/<Name>.client.tsx`, with a `default`
|
|
107
|
+
export, then regenerate packs: `bin/rails react_on_rails:generate_packs`.
|
|
108
|
+
- Or register manually: `ReactOnRails.register({ <Name>: <Name> });` and
|
|
109
|
+
`import <Name> from './components/<Name>';`.
|
|
110
|
+
- Check the name matches the `react_component("<Name>", ...)` call exactly (case-sensitive).
|
|
111
|
+
|
|
112
|
+
### `Auto-loaded Bundle Missing`
|
|
113
|
+
|
|
114
|
+
The component is set up for auto-loading but its bundle is missing.
|
|
115
|
+
|
|
116
|
+
1. Run the pack generation task: `bin/rails react_on_rails:generate_packs`.
|
|
117
|
+
2. Ensure the component is in the correct directory under `app/javascript`.
|
|
118
|
+
3. Check naming conventions: file is `<Name>.jsx` or `<Name>.tsx` and `export default`s.
|
|
119
|
+
4. Verify nested entries are enabled in your Shakapacker/webpack config.
|
|
120
|
+
|
|
121
|
+
### `Hydration Mismatch`
|
|
122
|
+
|
|
123
|
+
Server-rendered HTML doesn't match what React rendered on the client.
|
|
124
|
+
|
|
125
|
+
1. **Random IDs or timestamps**: use props or deterministic values, not
|
|
126
|
+
`Math.random()` / `Date.now()`.
|
|
127
|
+
2. **Browser-only APIs**: guard with `if (typeof window !== 'undefined') { ... }`.
|
|
128
|
+
3. **Different data**: ensure props/`railsContext`/store init are identical on
|
|
129
|
+
server and client.
|
|
130
|
+
4. Temporarily set `prerender: false` to isolate the issue.
|
|
131
|
+
|
|
132
|
+
### `Server Rendering Failed`
|
|
133
|
+
|
|
134
|
+
An error occurred while server-rendering a component.
|
|
135
|
+
|
|
136
|
+
1. Check JS console output in the Rails log: `tail -f log/development.log | grep 'React on Rails'`.
|
|
137
|
+
2. Common causes: missing Node dependencies, syntax errors, or browser-only APIs in
|
|
138
|
+
server code.
|
|
139
|
+
3. Set `config.trace = true` and check `config.server_bundle_js_file` points at the
|
|
140
|
+
correct file.
|
|
141
|
+
|
|
142
|
+
### `Redux Store Not Found`
|
|
143
|
+
|
|
144
|
+
A Redux store wasn't registered before a component that depends on it rendered.
|
|
145
|
+
|
|
146
|
+
1. Register the store: `ReactOnRails.registerStore({ <store> });`.
|
|
147
|
+
2. Initialize it in the view before the component: `<%= redux_store('<store>', props: {}) %>`.
|
|
148
|
+
3. Declare the dependency: `store_dependencies: ['<store>']`.
|
|
149
|
+
|
|
150
|
+
## 6. Diagnose your setup
|
|
151
|
+
|
|
152
|
+
Before guessing, run the doctor — it checks configuration, bundles, and dependencies:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bin/rails react_on_rails:doctor
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 7. Where the full reference lives
|
|
159
|
+
|
|
160
|
+
- Hosted docs: https://reactonrails.com/docs/
|
|
161
|
+
- Getting-started tutorial: https://reactonrails.com/docs/getting-started/tutorial/
|
|
162
|
+
- Machine-readable route map / expanded reference (for agents):
|
|
163
|
+
https://github.com/shakacode/react_on_rails/blob/main/llms.txt,
|
|
164
|
+
https://github.com/shakacode/react_on_rails/blob/main/llms-full.txt (OSS), and
|
|
165
|
+
https://github.com/shakacode/react_on_rails/blob/main/llms-full-pro.txt (Pro)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This app uses **React on Rails**. For React on Rails conventions — adding and
|
|
4
|
+
registering components, the `react_component` view helper, `.client`/`.server`
|
|
5
|
+
bundle rules, the `ReactOnRails` JS API, common errors and fixes, and the
|
|
6
|
+
`bin/rails react_on_rails:doctor` diagnostic — read **[AGENTS.md](./AGENTS.md)**.
|
|
7
|
+
|
|
8
|
+
Follow any project-specific instructions elsewhere in this file or repository.
|
|
@@ -16,7 +16,12 @@ class BundlerSwitcher
|
|
|
16
16
|
|
|
17
17
|
RSPACK_DEPS = {
|
|
18
18
|
dependencies: %w[@rspack/core@^2.0.0-0 rspack-manifest-plugin@^5.0.0],
|
|
19
|
-
dev_dependencies: %w[
|
|
19
|
+
dev_dependencies: %w[
|
|
20
|
+
@rspack/cli@^2.0.0-0
|
|
21
|
+
@rspack/dev-server@^2.0.0
|
|
22
|
+
@rspack/plugin-react-refresh@^2.0.0
|
|
23
|
+
react-refresh
|
|
24
|
+
]
|
|
20
25
|
}.freeze
|
|
21
26
|
|
|
22
27
|
def initialize(target_bundler)
|
data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt
CHANGED
|
@@ -11,7 +11,87 @@ const commonOptions = {
|
|
|
11
11
|
},
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
<% if use_tailwind? -%>
|
|
15
|
+
const tailwindPostcssPlugin = require('@tailwindcss/postcss');
|
|
16
|
+
|
|
17
|
+
const tailwindPostcssLoader = {
|
|
18
|
+
loader: 'postcss-loader',
|
|
19
|
+
options: {
|
|
20
|
+
postcssOptions: {
|
|
21
|
+
plugins: [tailwindPostcssPlugin],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const loaderName = (loader) => {
|
|
27
|
+
if (typeof loader === 'string') return loader;
|
|
28
|
+
if (loader && typeof loader.loader === 'string') return loader.loader;
|
|
29
|
+
return '';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const postcssLoaderUsesTailwind = (loader) => {
|
|
33
|
+
const plugins = loader?.options?.postcssOptions?.plugins || [];
|
|
34
|
+
const tailwindPluginName = tailwindPostcssPlugin.postcssPlugin || tailwindPostcssPlugin.name;
|
|
35
|
+
|
|
36
|
+
return plugins.some((plugin) => {
|
|
37
|
+
if (plugin === tailwindPostcssPlugin) return true;
|
|
38
|
+
if (!tailwindPluginName) return false;
|
|
39
|
+
|
|
40
|
+
return plugin?.postcssPlugin === tailwindPluginName || plugin?.name === tailwindPluginName;
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const mergeTailwindPostcssLoader = (loader) => {
|
|
45
|
+
if (typeof loader === 'string') return tailwindPostcssLoader;
|
|
46
|
+
if (postcssLoaderUsesTailwind(loader)) return loader;
|
|
47
|
+
|
|
48
|
+
const existingOptions = loader.options || {};
|
|
49
|
+
const existingPostcssOptions = existingOptions.postcssOptions || {};
|
|
50
|
+
const existingPlugins = existingPostcssOptions.plugins || [];
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...loader,
|
|
54
|
+
options: {
|
|
55
|
+
...existingOptions,
|
|
56
|
+
postcssOptions: {
|
|
57
|
+
...existingPostcssOptions,
|
|
58
|
+
plugins: [...existingPlugins, tailwindPostcssPlugin],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const addTailwindPostcssLoader = (webpackConfig) => {
|
|
65
|
+
const rules = webpackConfig.module?.rules;
|
|
66
|
+
if (!Array.isArray(rules)) return webpackConfig;
|
|
67
|
+
|
|
68
|
+
rules.forEach((rule) => {
|
|
69
|
+
if (!Array.isArray(rule.use)) return;
|
|
70
|
+
|
|
71
|
+
const postcssLoaderIndex = rule.use.findIndex((loader) => loaderName(loader).includes('postcss-loader'));
|
|
72
|
+
if (postcssLoaderIndex !== -1) {
|
|
73
|
+
rule.use[postcssLoaderIndex] = mergeTailwindPostcssLoader(rule.use[postcssLoaderIndex]);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cssLoaderIndex = rule.use.findIndex((loader) => loaderName(loader).includes('css-loader'));
|
|
78
|
+
if (cssLoaderIndex === -1) return;
|
|
79
|
+
|
|
80
|
+
rule.use.splice(cssLoaderIndex + 1, 0, tailwindPostcssLoader);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return webpackConfig;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
<% end -%>
|
|
14
87
|
// Copy the object using merge b/c the baseClientWebpackConfig and commonOptions are mutable globals
|
|
15
|
-
const commonWebpackConfig = () =>
|
|
88
|
+
const commonWebpackConfig = () => {
|
|
89
|
+
const webpackConfig = merge({}, baseClientWebpackConfig, commonOptions);
|
|
90
|
+
<% if use_tailwind? -%>
|
|
91
|
+
return addTailwindPostcssLoader(webpackConfig);
|
|
92
|
+
<% else -%>
|
|
93
|
+
return webpackConfig;
|
|
94
|
+
<% end -%>
|
|
95
|
+
};
|
|
16
96
|
|
|
17
97
|
module.exports = commonWebpackConfig;
|
|
@@ -10,8 +10,8 @@ const developmentEnvOnly = (clientWebpackConfig, _serverWebpackConfig) => {
|
|
|
10
10
|
// eslint-disable-next-line global-require
|
|
11
11
|
if (config.assets_bundler === 'rspack') {
|
|
12
12
|
// Rspack uses @rspack/plugin-react-refresh for React Fast Refresh
|
|
13
|
-
const
|
|
14
|
-
clientWebpackConfig.plugins.push(new
|
|
13
|
+
const { ReactRefreshRspackPlugin } = require('@rspack/plugin-react-refresh');
|
|
14
|
+
clientWebpackConfig.plugins.push(new ReactRefreshRspackPlugin());
|
|
15
15
|
} else {
|
|
16
16
|
// Webpack uses @pmmmwh/react-refresh-webpack-plugin
|
|
17
17
|
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import '../../../stylesheets/application.css';
|
|
3
|
+
|
|
4
|
+
const HelloWorld = (props) => {
|
|
5
|
+
const [name, setName] = useState(props.name);
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<section className="mx-auto max-w-xl rounded-lg border border-slate-200 bg-white p-6 text-slate-900 shadow-sm">
|
|
9
|
+
<p className="text-sm font-semibold uppercase text-sky-600">React on Rails + Tailwind CSS</p>
|
|
10
|
+
<h3 className="mt-2 text-2xl font-bold">Hello, {name}!</h3>
|
|
11
|
+
<p className="mt-3 text-sm leading-6 text-slate-600">
|
|
12
|
+
This component is server-rendered by Rails and styled by Tailwind CSS v4.
|
|
13
|
+
</p>
|
|
14
|
+
<form className="mt-5">
|
|
15
|
+
<label className="block text-sm font-medium text-slate-700" htmlFor="name">
|
|
16
|
+
Say hello to:
|
|
17
|
+
<input
|
|
18
|
+
id="name"
|
|
19
|
+
className="mt-2 w-full rounded-md border border-slate-300 px-3 py-2 shadow-sm focus:border-sky-500 focus:outline-hidden focus:ring-2 focus:ring-sky-200"
|
|
20
|
+
type="text"
|
|
21
|
+
value={name}
|
|
22
|
+
onChange={(e) => setName(e.target.value)}
|
|
23
|
+
/>
|
|
24
|
+
</label>
|
|
25
|
+
</form>
|
|
26
|
+
</section>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default HelloWorld;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import '../../../stylesheets/application.css';
|
|
3
|
+
|
|
4
|
+
interface HelloWorldProps {
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const HelloWorld: React.FC<HelloWorldProps> = (props) => {
|
|
9
|
+
const [name, setName] = useState(props.name);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<section className="mx-auto max-w-xl rounded-lg border border-slate-200 bg-white p-6 text-slate-900 shadow-sm">
|
|
13
|
+
<p className="text-sm font-semibold uppercase text-sky-600">React on Rails + Tailwind CSS</p>
|
|
14
|
+
<h3 className="mt-2 text-2xl font-bold">Hello, {name}!</h3>
|
|
15
|
+
<p className="mt-3 text-sm leading-6 text-slate-600">
|
|
16
|
+
This component is server-rendered by Rails and styled by Tailwind CSS v4.
|
|
17
|
+
</p>
|
|
18
|
+
<form className="mt-5">
|
|
19
|
+
<label className="block text-sm font-medium text-slate-700" htmlFor="name">
|
|
20
|
+
Say hello to:
|
|
21
|
+
<input
|
|
22
|
+
id="name"
|
|
23
|
+
className="mt-2 w-full rounded-md border border-slate-300 px-3 py-2 shadow-sm focus:border-sky-500 focus:outline-hidden focus:ring-2 focus:ring-sky-200"
|
|
24
|
+
type="text"
|
|
25
|
+
value={name}
|
|
26
|
+
onChange={(e) => setName(e.target.value)}
|
|
27
|
+
/>
|
|
28
|
+
</label>
|
|
29
|
+
</form>
|
|
30
|
+
</section>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default HelloWorld;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import prettier from 'eslint-config-prettier';
|
|
3
|
+
import importPlugin from 'eslint-plugin-import';
|
|
4
|
+
import react from 'eslint-plugin-react';
|
|
5
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
6
|
+
import globals from 'globals';
|
|
7
|
+
|
|
8
|
+
const reactHooksRecommendedLatest = reactHooks.configs['recommended-latest'];
|
|
9
|
+
const reactHooksRecommendedLatestConfigs = Array.isArray(reactHooksRecommendedLatest)
|
|
10
|
+
? reactHooksRecommendedLatest
|
|
11
|
+
: [reactHooksRecommendedLatest];
|
|
12
|
+
|
|
13
|
+
export default [
|
|
14
|
+
js.configs.recommended,
|
|
15
|
+
react.configs.flat.recommended,
|
|
16
|
+
importPlugin.flatConfigs.recommended,
|
|
17
|
+
...reactHooksRecommendedLatestConfigs,
|
|
18
|
+
{
|
|
19
|
+
files: ['**/*.{js,jsx,mjs,cjs}'],
|
|
20
|
+
languageOptions: {
|
|
21
|
+
ecmaVersion: 'latest',
|
|
22
|
+
sourceType: 'module',
|
|
23
|
+
globals: {
|
|
24
|
+
...globals.browser,
|
|
25
|
+
...globals.node,
|
|
26
|
+
...globals.mocha,
|
|
27
|
+
__DEBUG_SERVER_ERRORS__: true,
|
|
28
|
+
__SERVER_ERRORS__: true,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
settings: {
|
|
32
|
+
react: {
|
|
33
|
+
version: 'detect',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
rules: {
|
|
37
|
+
'no-console': 'off',
|
|
38
|
+
|
|
39
|
+
// https://github.com/import-js/eslint-plugin-import/issues/340
|
|
40
|
+
'import/no-extraneous-dependencies': 'off',
|
|
41
|
+
|
|
42
|
+
// The internal generated examples use local workspace packages during tests.
|
|
43
|
+
'import/no-unresolved': 'off',
|
|
44
|
+
|
|
45
|
+
'react/prop-types': 'off',
|
|
46
|
+
semi: 'off',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
prettier,
|
|
50
|
+
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const HelloWorld = ({ name, updateName }) => (
|
|
4
|
+
<section className="mx-auto max-w-xl rounded-lg border border-slate-200 bg-white p-6 text-slate-900 shadow-sm">
|
|
5
|
+
<p className="text-sm font-semibold uppercase text-sky-600">React on Rails + Redux + Tailwind CSS</p>
|
|
6
|
+
<h3 className="mt-2 text-2xl font-bold">Hello, {name}!</h3>
|
|
7
|
+
<p className="mt-3 text-sm leading-6 text-slate-600">
|
|
8
|
+
This Redux-backed component is server-rendered by Rails and styled by Tailwind CSS v4.
|
|
9
|
+
</p>
|
|
10
|
+
<div className="mt-5">
|
|
11
|
+
<label className="block text-sm font-medium text-slate-700" htmlFor="name">
|
|
12
|
+
Say hello to:
|
|
13
|
+
<input
|
|
14
|
+
id="name"
|
|
15
|
+
className="mt-2 w-full rounded-md border border-slate-300 px-3 py-2 shadow-sm focus:border-sky-500 focus:outline-hidden focus:ring-2 focus:ring-sky-200"
|
|
16
|
+
type="text"
|
|
17
|
+
value={name}
|
|
18
|
+
onChange={(e) => updateName(e.target.value)}
|
|
19
|
+
/>
|
|
20
|
+
</label>
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export default HelloWorld;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { PropsFromRedux } from '../containers/HelloWorldContainer';
|
|
3
|
+
|
|
4
|
+
// Component props are inferred from Redux container
|
|
5
|
+
type HelloWorldProps = PropsFromRedux;
|
|
6
|
+
|
|
7
|
+
const HelloWorld: React.FC<HelloWorldProps> = ({ name, updateName }) => (
|
|
8
|
+
<section className="mx-auto max-w-xl rounded-lg border border-slate-200 bg-white p-6 text-slate-900 shadow-sm">
|
|
9
|
+
<p className="text-sm font-semibold uppercase text-sky-600">React on Rails + Redux + Tailwind CSS</p>
|
|
10
|
+
<h3 className="mt-2 text-2xl font-bold">Hello, {name}!</h3>
|
|
11
|
+
<p className="mt-3 text-sm leading-6 text-slate-600">
|
|
12
|
+
This Redux-backed component is server-rendered by Rails and styled by Tailwind CSS v4.
|
|
13
|
+
</p>
|
|
14
|
+
<div className="mt-5">
|
|
15
|
+
<label className="block text-sm font-medium text-slate-700" htmlFor="name">
|
|
16
|
+
Say hello to:
|
|
17
|
+
<input
|
|
18
|
+
id="name"
|
|
19
|
+
className="mt-2 w-full rounded-md border border-slate-300 px-3 py-2 shadow-sm focus:border-sky-500 focus:outline-hidden focus:ring-2 focus:ring-sky-200"
|
|
20
|
+
type="text"
|
|
21
|
+
value={name}
|
|
22
|
+
onChange={(e) => updateName(e.target.value)}
|
|
23
|
+
/>
|
|
24
|
+
</label>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export default HelloWorld;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRails
|
|
4
|
+
module Controller
|
|
5
|
+
# Opt-in controller helpers for responding to `useRailsForm` submissions
|
|
6
|
+
# (packages/react-on-rails/src/useRailsForm.ts).
|
|
7
|
+
#
|
|
8
|
+
# The hook posts JSON and expects model validation failures as HTTP 422 with
|
|
9
|
+
# the body shape `{ "errors": { "field_name": ["message", ...] } }`. This
|
|
10
|
+
# concern renders exactly that shape from any object exposing ActiveModel
|
|
11
|
+
# errors, so a controller action can stay a plain Rails action:
|
|
12
|
+
#
|
|
13
|
+
# class ContactMessagesController < ApplicationController
|
|
14
|
+
# include ReactOnRails::Controller::FormResponders
|
|
15
|
+
#
|
|
16
|
+
# def create
|
|
17
|
+
# contact_message = ContactMessage.new(contact_message_params)
|
|
18
|
+
# if contact_message.save
|
|
19
|
+
# render json: { message: "Thanks!" }, status: :created
|
|
20
|
+
# else
|
|
21
|
+
# render_model_errors(contact_message)
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# Including this concern is optional — `useRailsForm` works against any
|
|
27
|
+
# endpoint that returns the documented shape.
|
|
28
|
+
module FormResponders
|
|
29
|
+
# Renders the validation errors of an ActiveModel/ActiveRecord object as
|
|
30
|
+
# JSON in the shape `useRailsForm` maps onto per-field errors.
|
|
31
|
+
#
|
|
32
|
+
# record: any object responding to `errors` with `ActiveModel::Errors`
|
|
33
|
+
# (or anything whose `errors` responds to `messages`).
|
|
34
|
+
# status: Numeric HTTP status for the response. Defaults to 422
|
|
35
|
+
# (Unprocessable Content), which is what the hook's error mapping
|
|
36
|
+
# keys on. Numeric statuses sidestep Rack/Rails status-symbol
|
|
37
|
+
# renames such as :unprocessable_entity to :unprocessable_content.
|
|
38
|
+
# SECURITY: ActiveModel error messages are sent to the browser verbatim.
|
|
39
|
+
# Review custom validations for internal IDs, admin-only details, or
|
|
40
|
+
# security-sensitive wording before using this on a model.
|
|
41
|
+
def render_model_errors(record, status: 422)
|
|
42
|
+
unless status.is_a?(Integer)
|
|
43
|
+
raise ArgumentError, "render_model_errors status must be an Integer HTTP status code"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless record.respond_to?(:errors)
|
|
47
|
+
raise ArgumentError, "render_model_errors record must respond to errors.messages"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
errors = record.errors
|
|
51
|
+
unless errors.respond_to?(:messages)
|
|
52
|
+
raise ArgumentError, "render_model_errors record must respond to errors.messages"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
render json: { errors: errors.messages }, status:
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -515,7 +515,7 @@ module ReactOnRails
|
|
|
515
515
|
when "webpack"
|
|
516
516
|
"config/webpack/development.js: ReactRefreshWebpackPlugin (enabled when WEBPACK_SERVE=true)"
|
|
517
517
|
when "rspack"
|
|
518
|
-
"config/rspack/development.js: @rspack/plugin-react-refresh /
|
|
518
|
+
"config/rspack/development.js: @rspack/plugin-react-refresh / ReactRefreshRspackPlugin " \
|
|
519
519
|
"(enabled for the dev server)"
|
|
520
520
|
else
|
|
521
521
|
"Check your bundler's React Refresh plugin documentation"
|