Skip to content

Commit ce10b69

Browse files
justin808claude
andauthored
Move React/Shakapacker version compatibility to generator smoke tests (#2125)
## Summary This PR moves React and Shakapacker version compatibility testing from spec/dummy to the generator smoke tests, as suggested in [PR #2114](#2114) review. Key changes: - Update spec/dummy to always use latest React 19 and Shakapacker 9.4.0 - Add minimum version example apps (`basic-minimum`, `basic-server-rendering-minimum`) that test React 18.0.0 and Shakapacker 8.2.0 - Simplify `script/convert` to only handle Node.js tooling compatibility (removed React/Shakapacker version modifications) - Update CI workflows to run appropriate examples per dependency level: - **Latest CI**: runs `run_rspec:shakapacker_examples_latest` (basic, redux, etc.) - **Minimum CI**: runs `run_rspec:shakapacker_examples_minimum` (basic-minimum, etc.) ## Benefits - **Clearer separation**: spec/dummy tests latest versions, generators test compatibility matrix - **Simpler CI configuration**: spec/dummy integration tests no longer need version conversions - **Better reflects real-world usage**: users running the generators get their compatibility tested ## Test plan - [x] Verify rake tasks are created correctly for new example types - [x] Verify `run_rspec:shakapacker_examples_latest` includes only non-minimum examples - [x] Verify `run_rspec:shakapacker_examples_minimum` includes only minimum examples - [x] RuboCop passes - [ ] CI runs generator tests with correct examples per dependency level ## Manual Testing Checklist Before merging, verify the following locally: ```bash # Verify TypeScript and linting pnpm run type-check pnpm run lint pnpm run build # Run react-on-rails package tests cd packages/react-on-rails && pnpm run test # Verify rake tasks exist and match expectations cd react_on_rails && bundle exec rake -T | grep shakapacker_examples # Test latest examples (if CI is running latest config) bundle exec rake run_rspec:shakapacker_examples_latest # Test minimum examples (if CI is running minimum config) bundle exec rake run_rspec:shakapacker_examples_pinned ``` Closes #2123 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** - CI now runs separate conditional example suites for latest and pinned/minimum setups. * **Tests** - Added targeted test tasks for React 16/17/18/19 and pinned groups; pinned suites run isolated installs and conversion steps. - Test adjusted to expect failure output on STDERR. * **Dependencies** - Upgraded React/React‑DOM and related types to v19; added RSC support package. * **Chores** - Helmet switched to provider-based usage; removed deprecated chromedriver-helper; improved example generation to support pinned React/Shakapacker versions. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <[email protected]>
1 parent 1fbbdc6 commit ce10b69

File tree

27 files changed

+491
-237
lines changed

27 files changed

+491
-237
lines changed

.github/workflows/examples.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ jobs:
137137
echo "Node version: "; node -v
138138
echo "pnpm version: "; pnpm --version
139139
echo "Bundler version: "; bundle --version
140-
- name: run conversion script to support shakapacker v6
140+
- name: Run conversion script for older Node compatibility
141141
if: matrix.dependency-level == 'minimum'
142142
run: script/convert
143143
- name: Save root ruby gems to cache
@@ -180,8 +180,24 @@ jobs:
180180
- name: Set packer version environment variable
181181
run: |
182182
echo "CI_DEPENDENCY_LEVEL=${{ matrix.dependency-level }}" >> $GITHUB_ENV
183-
- name: Main CI
184-
run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples
183+
- name: Verify rake tasks exist
184+
run: |
185+
cd react_on_rails
186+
echo "Available shakapacker_examples tasks:"
187+
bundle exec rake -T | grep shakapacker_examples || true
188+
# Verify the specific task we need exists
189+
TASK_NAME="run_rspec:shakapacker_examples_${{ matrix.dependency-level == 'latest' && 'latest' || 'pinned' }}"
190+
if ! bundle exec rake -T | grep -q "$TASK_NAME"; then
191+
echo "ERROR: Required rake task '$TASK_NAME' not found!"
192+
exit 1
193+
fi
194+
echo "✓ Found required task: $TASK_NAME"
195+
- name: Main CI - Latest version examples
196+
if: matrix.dependency-level == 'latest'
197+
run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_latest
198+
- name: "Main CI - Pinned version examples (React 16, 17, 18 with Shakapacker 8.2.0)"
199+
if: matrix.dependency-level == 'minimum'
200+
run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_pinned
185201
- name: Store test results
186202
uses: actions/upload-artifact@v4
187203
with:

.github/workflows/integration-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ jobs:
140140
echo "Node version: "; node -v
141141
echo "pnpm version: "; pnpm --version
142142
echo "Bundler version: "; bundle --version
143-
- name: run conversion script to support shakapacker v6
143+
- name: Run conversion script for older Node compatibility
144144
if: matrix.dependency-level == 'minimum'
145145
run: script/convert
146146
- name: Install Node modules with pnpm for renderer package
@@ -230,7 +230,7 @@ jobs:
230230
echo "Node version: "; node -v
231231
echo "pnpm version: "; pnpm --version
232232
echo "Bundler version: "; bundle --version
233-
- name: run conversion script to support shakapacker v6
233+
- name: Run conversion script for older Node compatibility
234234
if: matrix.dependency-level == 'minimum'
235235
run: script/convert
236236
- name: Save root ruby gems to cache

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
"@tsconfig/node14": "^14.1.2",
3232
"@types/jest": "^29.5.14",
3333
"@types/node": "^20.17.16",
34-
"@types/react": "^18.3.18",
35-
"@types/react-dom": "^18.3.5",
34+
"@types/react": "^19.0.0",
35+
"@types/react-dom": "^19.0.0",
3636
"@types/turbolinks": "^5.2.2",
3737
"create-react-class": "^15.7.0",
3838
"eslint": "^9.24.0",
@@ -58,8 +58,9 @@
5858
"prettier": "^3.5.2",
5959
"prop-types": "^15.8.1",
6060
"publint": "^0.3.4",
61-
"react": "18.0.0",
62-
"react-dom": "18.0.0",
61+
"react": "^19.0.0",
62+
"react-dom": "^19.0.0",
63+
"react-on-rails-rsc": "19.0.2",
6364
"redux": "^4.2.1",
6465
"size-limit": "^12.0.0",
6566
"stylelint": "^16.14.0",

packages/react-on-rails/src/ClientRenderer.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ function unmountAllComponents(): void {
174174
root.unmount();
175175
} else {
176176
// React 16-17 legacy API
177-
// eslint-disable-next-line @typescript-eslint/no-deprecated
178177
unmountComponentAtNode(domNode);
179178
}
180179
} catch (error) {

packages/react-on-rails/src/reactApis.cts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import * as ReactDOM from 'react-dom';
44
import type { ReactElement } from 'react';
55
import type { RenderReturnType } from './types/index.ts' with { 'resolution-mode': 'import' };
66

7+
// Type for legacy React DOM APIs (React 16/17) that were removed from @types/react-dom@19
8+
// These are only used at runtime when supportsRootApi is false
9+
interface LegacyReactDOM {
10+
hydrate(element: ReactElement, container: Element): void;
11+
render(element: ReactElement, container: Element): RenderReturnType;
12+
unmountComponentAtNode(container: Element): boolean;
13+
}
14+
715
const reactMajorVersion = Number(ReactDOM.version?.split('.')[0]) || 16;
816

917
// TODO: once we require React 18, we can remove this and inline everything guarded by it.
@@ -29,12 +37,27 @@ if (supportsRootApi) {
2937

3038
type HydrateOrRenderType = (domNode: Element, reactElement: ReactElement) => RenderReturnType;
3139

32-
/* eslint-disable @typescript-eslint/no-deprecated,@typescript-eslint/no-non-null-assertion,react/no-deprecated --
33-
* while we need to support React 16
34-
*/
40+
// Cast ReactDOM to include legacy APIs for React 16/17 compatibility
41+
// These methods exist at runtime but are removed from @types/react-dom@19
42+
const legacyReactDOM = ReactDOM as unknown as LegacyReactDOM;
43+
44+
// Validate legacy APIs exist at runtime when needed (React < 18)
45+
if (!supportsRootApi) {
46+
if (typeof legacyReactDOM.hydrate !== 'function') {
47+
throw new Error('React legacy hydrate API not available. Expected React 16/17.');
48+
}
49+
if (typeof legacyReactDOM.render !== 'function') {
50+
throw new Error('React legacy render API not available. Expected React 16/17.');
51+
}
52+
if (typeof legacyReactDOM.unmountComponentAtNode !== 'function') {
53+
throw new Error('React legacy unmountComponentAtNode API not available. Expected React 16/17.');
54+
}
55+
}
56+
57+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- reactDomClient is always defined when supportsRootApi is true */
3558
export const reactHydrate: HydrateOrRenderType = supportsRootApi
3659
? reactDomClient!.hydrateRoot
37-
: (domNode, reactElement) => ReactDOM.hydrate(reactElement, domNode);
60+
: (domNode, reactElement) => legacyReactDOM.hydrate(reactElement, domNode);
3861

3962
export function reactRender(domNode: Element, reactElement: ReactElement): RenderReturnType {
4063
if (supportsRootApi) {
@@ -43,14 +66,14 @@ export function reactRender(domNode: Element, reactElement: ReactElement): Rende
4366
return root;
4467
}
4568

46-
// eslint-disable-next-line react/no-render-return-value
47-
return ReactDOM.render(reactElement, domNode);
69+
return legacyReactDOM.render(reactElement, domNode);
4870
}
4971

50-
export const unmountComponentAtNode: typeof ReactDOM.unmountComponentAtNode = supportsRootApi
72+
export const unmountComponentAtNode: (container: Element) => boolean = supportsRootApi
5173
? // not used if we use root API
52-
() => false
53-
: ReactDOM.unmountComponentAtNode;
74+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
75+
(_container: Element) => false
76+
: (container: Element) => legacyReactDOM.unmountComponentAtNode(container);
5477

5578
export const ensureReactUseAvailable = () => {
5679
if (!('use' in React) || typeof React.use !== 'function') {

packages/react-on-rails/src/types/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,9 @@ interface ServerRenderResult {
125125
error?: Error;
126126
}
127127

128-
type CreateReactOutputSyncResult = ServerRenderResult | ReactElement<unknown>;
128+
type CreateReactOutputSyncResult = ServerRenderResult | ReactElement;
129129

130-
type CreateReactOutputAsyncResult = Promise<string | ServerRenderHashRenderedHtml | ReactElement<unknown>>;
130+
type CreateReactOutputAsyncResult = Promise<string | ServerRenderHashRenderedHtml | ReactElement>;
131131

132132
type CreateReactOutputResult = CreateReactOutputSyncResult | CreateReactOutputAsyncResult;
133133

0 commit comments

Comments
 (0)