diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6d318fd4..5d38a99ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ Changes since the last non-beta release. #### Bug Fixes +- **React 19 Compatibility**: Fixed compatibility with React 19 by ensuring Pro package (RSCProvider, RSCRoute) uses named imports from React instead of namespace imports. This prevents build errors when esModuleInterop is disabled in TypeScript configuration. [PR 1937](https://github.com/shakacode/react_on_rails/pull/1937) by [justin808](https://github.com/justin808). + - **Use as Git dependency**: All packages can now be installed as Git dependencies. This is useful for development and testing purposes. See [CONTRIBUTING.md](./CONTRIBUTING.md#git-dependencies) for documentation. [PR #1873](https://github.com/shakacode/react_on_rails/pull/1873) by [alexeyr-ci2](https://github.com/alexeyr-ci2). #### Breaking Changes diff --git a/docs/upgrading/react-19-quick-reference.md b/docs/upgrading/react-19-quick-reference.md new file mode 100644 index 0000000000..1d3da9b9f1 --- /dev/null +++ b/docs/upgrading/react-19-quick-reference.md @@ -0,0 +1,172 @@ +# React 19 Quick Reference + +> For the complete guide, see [React 19 Upgrade Guide](./react-19-upgrade-guide.md) + +## Pre-Flight Checklist + +```bash +# ✅ Check current versions +node --version # Should be 18+ +ruby --version # Should be 3.2+ + +# ✅ Check dependencies +bundle info react_on_rails # Should be 16.1.1+ +bundle info shakapacker # Should be 9.0+ +``` + +## Upgrade Commands + +```bash +# 1. Update React +yarn add react@19.0.0 react-dom@19.0.0 +yarn add -D @types/react@19 @types/react-dom@19 + +# 2. Update React on Rails (if needed) +bundle update react_on_rails + +# 3. Rebuild +rm -rf public/packs node_modules/.cache +yarn install +bin/rails assets:precompile +``` + +## Common Import Fixes + +### ❌ BROKEN (with esModuleInterop: false) + +```typescript +import * as React from 'react'; + +class MyComponent extends React.Component {} +const [count, setCount] = React.useState(0); +const context = React.createContext(null); +``` + +**Error**: `export 'createContext' was not found in 'react'` + +### ✅ FIXED - Option A: Enable esModuleInterop + +```json +{ + "compilerOptions": { + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + } +} +``` + +```typescript +import React, { useState, createContext } from 'react'; + +class MyComponent extends React.Component {} +const [count, setCount] = useState(0); +const context = createContext(null); +``` + +### ✅ FIXED - Option B: Use Named Imports + +```typescript +import { Component, useState, createContext } from 'react'; + +class MyComponent extends Component {} +const [count, setCount] = useState(0); +const context = createContext(null); +``` + +## RSC Components (Pro) + +```typescript +// ✅ Server Component - NO 'use client' +export default async function ServerComponent() { + const data = await fetchData(); + return
{data}
; +} + +// ✅ Client Component - HAS 'use client' +'use client'; + +import { useState } from 'react'; + +export default function ClientComponent() { + const [count, setCount] = useState(0); + return ; +} + +// ✅ RSC Provider/Route - MUST use named imports +'use client'; + +import { createContext, useContext } from 'react'; + +const MyContext = createContext(null); +export const useMyContext = () => useContext(MyContext); +``` + +## Build Verification + +```bash +# Should complete without React import errors +bin/shakapacker + +# Check for these errors (should be NONE): +# ❌ export 'createContext' was not found +# ❌ export 'useContext' was not found +# ❌ export 'Component' was not found +``` + +## Troubleshooting + +| Error | Quick Fix | +| ------------------------------------------ | ------------------------------------------------------ | +| `export 'X' was not found in 'react'` | Use named imports or enable `esModuleInterop` | +| `Objects are not valid as a React child` | Add second param to render function | +| `Functions are not valid as a React child` | Return React element, not function | +| Build fails in production | Clear cache: `rm -rf node_modules/.cache public/packs` | + +## TypeScript Config + +### Recommended (Easy Mode) + +```json +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "jsx": "react-jsx", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "bundler" + } +} +``` + +### Advanced (esModuleInterop: false) + +```json +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "jsx": "react-jsx", + "esModuleInterop": false, + "moduleResolution": "bundler" + } +} +``` + +**MUST use named imports everywhere!** + +## Rollback + +```bash +yarn add react@18.3.1 react-dom@18.3.1 +yarn add -D @types/react@18 @types/react-dom@18 +rm -rf public/packs +bin/rails assets:precompile +``` + +## Need Help? + +- [Full React 19 Upgrade Guide](./react-19-upgrade-guide.md) +- [React on Rails Issues](https://github.com/shakacode/react_on_rails/issues) +- [ShakaCode Forum](https://forum.shakacode.com) +- [Commercial Support](mailto:justin@shakacode.com) diff --git a/docs/upgrading/react-19-upgrade-guide.md b/docs/upgrading/react-19-upgrade-guide.md new file mode 100644 index 0000000000..a0e635c15a --- /dev/null +++ b/docs/upgrading/react-19-upgrade-guide.md @@ -0,0 +1,422 @@ +# React 19 Upgrade Guide + +## Overview + +This guide covers upgrading React on Rails applications from React 18 to React 19. React 19 introduces several breaking changes that affect how React on Rails works, particularly around module exports and TypeScript compilation. + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Breaking Changes](#breaking-changes) +- [Upgrade Steps](#upgrade-steps) +- [Common Issues & Solutions](#common-issues--solutions) +- [TypeScript Configuration](#typescript-configuration) +- [React Server Components (Pro)](#react-server-components-pro) +- [Testing Your Upgrade](#testing-your-upgrade) + +## Prerequisites + +Before upgrading to React 19, ensure you have: + +- **React on Rails 16.1.1+** - Earlier versions do not support React 19 +- **Shakapacker 9.0+** - Required for proper module resolution +- **Node.js 18+** - Recommended for React 19 +- **TypeScript 5.0+** (if using TypeScript) + +## Breaking Changes + +### 1. Conditional Package Exports + +React 19 introduced conditional exports in its `package.json`: + +```json +{ + "exports": { + ".": { + "react-server": "./react.react-server.js", + "default": "./index.js" + } + } +} +``` + +**Impact**: This change affects how bundlers resolve React modules. The `react-server` condition exports a server-only build without hooks, Context API, or Component class. + +**Solution**: Ensure your webpack/bundler configuration properly handles these conditions. React on Rails 16.1.1+ includes the necessary configuration. + +### 2. No Default Export from react/index.js + +React 19 removed the default export from the internal `react/index.js` file. + +**Impact**: Code that directly imports from `react/index.js` will fail: + +```javascript +// ❌ BROKEN - No default export +import ReactClient from 'react/index.js'; + +// ✅ WORKS - Use named imports or namespace import +import { createContext, useContext } from 'react'; +// OR +import * as React from 'react'; +``` + +**Solution**: Always import from the main `'react'` package, not internal paths. + +### 3. TypeScript esModuleInterop Issues + +With `esModuleInterop: false` in `tsconfig.json`, TypeScript may incorrectly compile: + +```typescript +import * as React from 'react'; +``` + +Into: + +```javascript +import ReactClient from 'react/index.js'; // ❌ BROKEN +``` + +**Solution**: Use named imports or configure TypeScript properly (see [TypeScript Configuration](#typescript-configuration)). + +### 4. Removed or Deprecated APIs + +React 19 removed several deprecated APIs: + +- `React.createFactory()` - Use JSX instead +- Legacy Context (`contextTypes`, `getChildContext`) - Use `React.createContext()` +- String refs - Use callback refs or `useRef()` +- `defaultProps` for function components - Use default parameters + +**Migration**: Update your code to use modern React APIs before upgrading. + +## Upgrade Steps + +### Step 1: Update Dependencies + +```bash +# Update React and React DOM +yarn add react@19.0.0 react-dom@19.0.0 + +# Update React on Rails (if not already on 16.1.1+) +bundle update react_on_rails + +# Update types (if using TypeScript) +yarn add -D @types/react@19 @types/react-dom@19 +``` + +### Step 2: Update TypeScript Configuration (if applicable) + +If you're using TypeScript, update your `tsconfig.json`: + +```json +{ + "compilerOptions": { + // Option A: Enable esModuleInterop (recommended) + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + + // Option B: If you must keep esModuleInterop: false, + // use named imports everywhere (see below) + } +} +``` + +### Step 3: Update Import Statements + +If you have `esModuleInterop: false`, update all React imports to use named imports: + +**Before (React 18):** + +```typescript +import * as React from 'react'; +import { useState } from 'react'; + +const MyComponent = () => { + const [count, setCount] = React.useState(0); + return
{count}
; +}; +``` + +**After (React 19 with esModuleInterop: false):** + +```typescript +import { useState, type ReactNode, type FC } from 'react'; + +const MyComponent: FC = () => { + const [count, setCount] = useState(0); + return
{count}
; +}; +``` + +**OR with esModuleInterop: true:** + +```typescript +import React, { useState } from 'react'; + +const MyComponent = () => { + const [count, setCount] = useState(0); + return
{count}
; +}; +``` + +### Step 4: Update React on Rails Registration + +No changes needed for component registration. Your existing code should work: + +```javascript +import ReactOnRails from 'react-on-rails'; +import MyComponent from './MyComponent'; + +ReactOnRails.register({ MyComponent }); +``` + +### Step 5: Rebuild Your Assets + +```bash +# Clear any cached builds +rm -rf public/packs + +# Rebuild +bin/rails assets:precompile +# OR if using Shakapacker in development +bin/shakapacker +``` + +## Common Issues & Solutions + +### Issue: "export 'createContext' was not found in 'react'" + +**Cause**: TypeScript is generating invalid imports due to `esModuleInterop: false`. + +**Solution**: + +1. Enable `esModuleInterop: true` in `tsconfig.json`, OR +2. Use named imports everywhere: + ```typescript + import { createContext, useContext } from 'react'; + ``` + +### Issue: "export 'Component' was not found in 'react'" + +**Cause**: Same as above - TypeScript compilation issue. + +**Solution**: Change from: + +```typescript +import * as React from 'react'; +class MyComponent extends React.Component {} +``` + +To: + +```typescript +import { Component } from 'react'; +class MyComponent extends Component {} +``` + +### Issue: Server bundle fails with React import errors + +**Cause**: The server webpack bundle is incorrectly resolving to the `react-server` condition. + +**Solution**: Ensure your server webpack config doesn't include `'react-server'` in `resolve.conditionNames` unless you're building an RSC bundle. React on Rails handles this automatically. + +### Issue: Third-party libraries fail with React import errors + +**Cause**: Libraries in `node_modules` may have dependencies that use namespace imports. + +**Solutions**: + +1. Update the library to a React 19-compatible version +2. Add the library to webpack externals if it's server-only +3. Use webpack's `resolve.alias` to ensure React resolves correctly + +## TypeScript Configuration + +### Recommended Configuration for React 19 + +```json +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "bundler", + + // RECOMMENDED: Enable for easier React imports + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + + // Type checking + "strict": true, + "skipLibCheck": true, + + // React 19 specific + "types": ["react", "react-dom"] + } +} +``` + +### If You Must Keep esModuleInterop: false + +```json +{ + "compilerOptions": { + "esModuleInterop": false, + "allowSyntheticDefaultImports": false, + + // Use these settings + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "bundler" + } +} +``` + +**Important**: With `esModuleInterop: false`, you MUST use named imports: + +```typescript +// ✅ CORRECT +import { useState, useEffect, type FC } from 'react'; + +// ❌ WRONG - Will generate invalid code +import * as React from 'react'; +React.useState(); // This will fail to compile correctly +``` + +## React Server Components (Pro) + +If you're using React on Rails Pro with React Server Components: + +### Update Pro Package + +```bash +# Ensure you're on React on Rails Pro 16.1.1+ +bundle update react_on_rails_pro +``` + +### RSC-Specific Considerations + +1. **Named Imports Required**: RSC components MUST use named imports: + +```typescript +// ✅ CORRECT for RSC +import { createContext, useContext } from 'react'; + +// ❌ WRONG - Breaks with React 19 +import * as React from 'react'; +``` + +2. **'use client' Directive**: Client components still need the directive: + +```typescript +'use client'; + +import { useState } from 'react'; + +export default function ClientComponent() { + const [count, setCount] = useState(0); + return ; +} +``` + +3. **Server Components**: No 'use client' directive needed: + +```typescript +// No directive - this is a server component +export default async function ServerComponent() { + const data = await fetchData(); + return
{data}
; +} +``` + +### RSC Bundle Configuration + +React on Rails Pro automatically configures the RSC bundle to use the correct React build. No manual configuration needed. + +## Testing Your Upgrade + +### 1. Run Your Test Suite + +```bash +# Ruby tests +bundle exec rspec + +# JavaScript tests +yarn test +``` + +### 2. Build in Development + +```bash +# Start your dev server +bin/dev + +# Or manually build +bin/shakapacker +``` + +Check the console for: + +- ❌ No webpack errors about React imports +- ❌ No "export not found" errors +- ✅ Clean build with no warnings + +### 3. Test Server-Side Rendering + +If you use SSR, test a server-rendered page: + +```bash +# Start your Rails server +bin/rails s + +# Visit a page with server-rendered React +curl http://localhost:3000/your-page | grep "data-react" +``` + +### 4. Test in Production Mode + +```bash +RAILS_ENV=production NODE_ENV=production bin/rails assets:precompile +``` + +Ensure no errors during compilation. + +## Rollback Plan + +If you encounter issues and need to rollback: + +```bash +# Downgrade React +yarn add react@18.3.1 react-dom@18.3.1 + +# Downgrade types (if using TypeScript) +yarn add -D @types/react@18 @types/react-dom@18 + +# Rebuild +rm -rf public/packs +bin/rails assets:precompile +``` + +## Additional Resources + +- [Official React 19 Upgrade Guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) +- [React 19 Release Notes](https://react.dev/blog/2024/12/05/react-19) +- [Shakapacker Documentation](https://github.com/shakacode/shakapacker) +- [React on Rails Documentation](https://www.shakacode.com/react-on-rails/docs/) + +## Getting Help + +If you encounter issues during the upgrade: + +1. **Check the Issues**: [React on Rails GitHub Issues](https://github.com/shakacode/react_on_rails/issues) +2. **Community Forum**: [ShakaCode Forum](https://forum.shakacode.com) +3. **Discord**: [React on Rails Discord](https://discord.gg/reactonrails) +4. **Commercial Support**: Contact [ShakaCode](mailto:justin@shakacode.com) for professional support + +## Changelog + +- **2025-11-06**: Initial React 19 upgrade guide created +- Added TypeScript configuration guidelines +- Added React Server Components section +- Added common troubleshooting scenarios diff --git a/docs/upgrading/upgrading-react-on-rails.md b/docs/upgrading/upgrading-react-on-rails.md index 5a04d10a41..1d69f9943a 100644 --- a/docs/upgrading/upgrading-react-on-rails.md +++ b/docs/upgrading/upgrading-react-on-rails.md @@ -21,6 +21,16 @@ rails generate react_on_rails:install - `shakapacker.yml` settings - other configuration files +## Upgrading to React 19 + +If you're upgrading to React 19, please see the comprehensive [React 19 Upgrade Guide](./react-19-upgrade-guide.md). + +**Key Changes in React 19:** + +- Conditional package exports affect module resolution +- TypeScript import patterns may need updates +- React Server Components require named imports + ## Upgrading to v16 ### Breaking Changes diff --git a/eslint-rules/README.md b/eslint-rules/README.md new file mode 100644 index 0000000000..ed543449e1 --- /dev/null +++ b/eslint-rules/README.md @@ -0,0 +1,110 @@ +# Custom ESLint Rules + +This directory contains custom ESLint rules specific to React on Rails. + +## Rules + +### `no-use-client-in-server-files` + +Prevents the `'use client'` directive from being used in `.server.tsx` and `.server.ts` files. + +#### Why This Rule Exists + +Files ending with `.server.tsx` are intended for server-side rendering in React Server Components (RSC) architecture. The `'use client'` directive forces webpack to bundle these files as client components, which creates a fundamental contradiction and causes errors when using React's `react-server` conditional exports. + +This issue became apparent with Shakapacker 9.3.0+, which properly honors `resolve.conditionNames` in webpack configurations. When webpack resolves imports with the `react-server` condition, React's server exports intentionally omit client-only APIs like: + +- `createContext`, `useContext` +- `useState`, `useEffect`, `useLayoutEffect`, `useReducer` +- `Component`, `PureComponent` +- Other hooks (`use*` functions) + +#### Examples + +❌ **Incorrect** - Will trigger an error: + +```typescript +// Component.server.tsx +'use client'; + +import React from 'react'; + +export function MyComponent() { + return
Component
; +} +``` + +✅ **Correct** - No directive in server files: + +```typescript +// Component.server.tsx +import React from 'react'; + +export function MyComponent() { + return
Component
; +} +``` + +✅ **Correct** - Use `'use client'` in client files: + +```typescript +// Component.client.tsx or Component.tsx +'use client'; + +import React, { useState } from 'react'; + +export function MyComponent() { + const [count, setCount] = useState(0); + return
Count: {count}
; +} +``` + +#### Auto-fix + +This rule includes an automatic fixer that will remove the `'use client'` directive from `.server.tsx` files when you run ESLint with the `--fix` option: + +```bash +npx eslint --fix path/to/file.server.tsx +``` + +#### Related + +- **Issue:** [Shakapacker #805 - Breaking change in 9.3.0](https://github.com/shakacode/shakapacker/issues/805) +- **Fix PR:** [React on Rails #1896](https://github.com/shakacode/react_on_rails/pull/1896) +- **Commit:** [86979dca - Remove 'use client' from .server.tsx files](https://github.com/shakacode/react_on_rails/commit/86979dca) + +#### Configuration + +This rule is automatically enabled in the React on Rails ESLint configuration at the `error` level. It's defined in `eslint.config.ts`: + +```typescript +plugins: { + 'react-on-rails': { + rules: { + 'no-use-client-in-server-files': noUseClientInServerFiles, + }, + }, +}, + +rules: { + 'react-on-rails/no-use-client-in-server-files': 'error', + // ... other rules +} +``` + +## Testing + +To run tests for the custom rules: + +```bash +node eslint-rules/no-use-client-in-server-files.test.cjs +``` + +## Adding New Custom Rules + +To add a new custom ESLint rule: + +1. Create the rule file in this directory (use `.cjs` extension for CommonJS) +2. Create a corresponding test file (e.g., `rule-name.test.cjs`) +3. Import and register the rule in `eslint.config.ts` +4. Add documentation to this README diff --git a/eslint.config.ts b/eslint.config.ts index 20a11aaa9b..618016ee33 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -85,7 +85,21 @@ const config = tsEslint.config([ }, }, + plugins: { + 'react-on-rails': { + rules: { + 'no-use-client-in-server-files': noUseClientInServerFiles, + }, + }, + }, + rules: { + // Custom React on Rails rules + 'react-on-rails/no-use-client-in-server-files': 'error', + + // React 17+ with new JSX transform doesn't require React in scope + 'react/react-in-jsx-scope': 'off', + 'no-shadow': 'off', 'no-console': 'off', 'function-paren-newline': 'off', @@ -160,13 +174,6 @@ const config = tsEslint.config([ }, { files: ['**/*.server.ts', '**/*.server.tsx'], - plugins: { - 'react-on-rails': { - rules: { - 'no-use-client-in-server-files': noUseClientInServerFiles, - }, - }, - }, rules: { 'react-on-rails/no-use-client-in-server-files': 'error', }, diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt b/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt index ec6e527918..f4d0e8734f 100644 --- a/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt @@ -50,6 +50,8 @@ const configureServer = () => { serverWebpackConfig.output = { filename: 'server-bundle.js', globalObject: 'this', + // Note: libraryTarget should only be 'commonjs2' when using Node renderer + // For ExecJS (default), leave it undefined to allow proper evaluation // If using the React on Rails Pro node server renderer, uncomment the next line // libraryTarget: 'commonjs2', path: require('path').resolve(__dirname, '../../ssr-generated'), @@ -111,8 +113,18 @@ const configureServer = () => { // If using the default 'web', then libraries like Emotion and loadable-components // break with SSR. The fix is to use a node renderer and change the target. - // If using the React on Rails Pro node server renderer, uncomment the next line - // serverWebpackConfig.target = 'node' + // React 19 requires target: 'node' to properly handle Node.js built-in modules + serverWebpackConfig.target = 'node'; + + // React 19 Fix: Prevent webpack from resolving to react-server condition + // The server-bundle needs the full React with hooks for SSR, not the react-server build + // Explicitly set conditionNames without react-server + if (!serverWebpackConfig.resolve) { + serverWebpackConfig.resolve = {}; + } + // For target: 'node', webpack defaults to ['node', 'import', 'require', 'default'] + // We explicitly list them to ensure react-server is not included + serverWebpackConfig.resolve.conditionNames = ['node', 'import', 'require', 'default']; return serverWebpackConfig; }; diff --git a/packages/react-on-rails-pro/src/RSCProvider.tsx b/packages/react-on-rails-pro/src/RSCProvider.tsx index 3a25161d0e..11689d5012 100644 --- a/packages/react-on-rails-pro/src/RSCProvider.tsx +++ b/packages/react-on-rails-pro/src/RSCProvider.tsx @@ -12,17 +12,19 @@ * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md */ -import * as React from 'react'; +'use client'; + +import { createContext, useContext, type ReactNode } from 'react'; import type { ClientGetReactServerComponentProps } from './getReactServerComponent.client.ts'; import { createRSCPayloadKey } from './utils.ts'; type RSCContextType = { - getComponent: (componentName: string, componentProps: unknown) => Promise; + getComponent: (componentName: string, componentProps: unknown) => Promise; - refetchComponent: (componentName: string, componentProps: unknown) => Promise; + refetchComponent: (componentName: string, componentProps: unknown) => Promise; }; -const RSCContext = React.createContext(undefined); +const RSCContext = createContext(undefined); /** * Creates a provider context for React Server Components. @@ -46,9 +48,9 @@ const RSCContext = React.createContext(undefined); export const createRSCProvider = ({ getServerComponent, }: { - getServerComponent: (props: ClientGetReactServerComponentProps) => Promise; + getServerComponent: (props: ClientGetReactServerComponentProps) => Promise; }) => { - const fetchRSCPromises: Record> = {}; + const fetchRSCPromises: Record> = {}; const getComponent = (componentName: string, componentProps: unknown) => { const key = createRSCPayloadKey(componentName, componentProps); @@ -74,7 +76,7 @@ export const createRSCProvider = ({ const contextValue = { getComponent, refetchComponent }; - return ({ children }: { children: React.ReactNode }) => { + return ({ children }: { children: ReactNode }) => { return {children}; }; }; @@ -99,7 +101,7 @@ export const createRSCProvider = ({ * ``` */ export const useRSC = () => { - const context = React.useContext(RSCContext); + const context = useContext(RSCContext); if (!context) { throw new Error('useRSC must be used within a RSCProvider'); } diff --git a/packages/react-on-rails-pro/src/RSCRoute.tsx b/packages/react-on-rails-pro/src/RSCRoute.tsx index 84d2a4b34c..7a8a042545 100644 --- a/packages/react-on-rails-pro/src/RSCRoute.tsx +++ b/packages/react-on-rails-pro/src/RSCRoute.tsx @@ -16,7 +16,7 @@ 'use client'; -import * as React from 'react'; +import { Component, use, type ReactNode } from 'react'; import { useRSC } from './RSCProvider.tsx'; import { ServerComponentFetchError } from './ServerComponentFetchError.ts'; @@ -24,11 +24,11 @@ import { ServerComponentFetchError } from './ServerComponentFetchError.ts'; * Error boundary component for RSCRoute that adds server component name and props to the error * So, the parent ErrorBoundary can refetch the server component */ -class RSCRouteErrorBoundary extends React.Component< - { children: React.ReactNode; componentName: string; componentProps: unknown }, +class RSCRouteErrorBoundary extends Component< + { children: ReactNode; componentName: string; componentProps: unknown }, { error: Error | null } > { - constructor(props: { children: React.ReactNode; componentName: string; componentProps: unknown }) { + constructor(props: { children: ReactNode; componentName: string; componentProps: unknown }) { super(props); this.state = { error: null }; } @@ -75,9 +75,9 @@ export type RSCRouteProps = { componentProps: unknown; }; -const PromiseWrapper = ({ promise }: { promise: Promise }) => { - // React.use is available in React 18.3+ - const promiseResult = React.use(promise); +const PromiseWrapper = ({ promise }: { promise: Promise }) => { + // use is available in React 18.3+ + const promiseResult = use(promise); // In case that an error happened during the rendering of the RSC payload before the rendering of the component itself starts // RSC bundle will return an error object serialized inside the RSC payload @@ -88,7 +88,7 @@ const PromiseWrapper = ({ promise }: { promise: Promise }) => { return promiseResult; }; -const RSCRoute = ({ componentName, componentProps }: RSCRouteProps): React.ReactNode => { +const RSCRoute = ({ componentName, componentProps }: RSCRouteProps): ReactNode => { const { getComponent } = useRSC(); const componentPromise = getComponent(componentName, componentProps); return ( diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncOnServerSyncOnClient.server.tsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncOnServerSyncOnClient.server.tsx index 43a81025e5..71f0f0448e 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncOnServerSyncOnClient.server.tsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/AsyncOnServerSyncOnClient.server.tsx @@ -1,5 +1,3 @@ -'use client'; - import wrapServerComponentRenderer from 'react-on-rails-pro/wrapServerComponentRenderer/server'; import AsyncOnServerSyncOnClient from '../components/AsyncOnServerSyncOnClient'; diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx index 22c3e3d2eb..23d92a1222 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx @@ -1,5 +1,3 @@ -'use client'; - import React from 'react'; import { renderToString } from 'react-dom/server'; import { getMarkupFromTree } from '@apollo/client/react/ssr'; diff --git a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ServerComponentRouter.server.tsx b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ServerComponentRouter.server.tsx index bb32707ecd..9ff1886628 100644 --- a/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ServerComponentRouter.server.tsx +++ b/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ServerComponentRouter.server.tsx @@ -1,5 +1,3 @@ -'use client'; - import * as React from 'react'; import { StaticRouter } from 'react-router-dom/server.js'; import { RailsContext, ReactComponentOrRenderFunction } from 'react-on-rails-pro'; diff --git a/react_on_rails_pro/spec/dummy/config/webpack/serverWebpackConfig.js b/react_on_rails_pro/spec/dummy/config/webpack/serverWebpackConfig.js index f47ea2ba26..4111738640 100644 --- a/react_on_rails_pro/spec/dummy/config/webpack/serverWebpackConfig.js +++ b/react_on_rails_pro/spec/dummy/config/webpack/serverWebpackConfig.js @@ -123,6 +123,16 @@ const configureServer = (rscBundle = false) => { serverWebpackConfig.node = false; + // React 19 Fix: Prevent webpack from resolving to react-server condition + // The server-bundle needs the full React with hooks for SSR, not the react-server build + // Explicitly set conditionNames without react-server + if (!serverWebpackConfig.resolve) { + serverWebpackConfig.resolve = {}; + } + // For target: 'node', webpack defaults to ['node', 'import', 'require', 'default'] + // We explicitly list them to ensure react-server is not included + serverWebpackConfig.resolve.conditionNames = ['node', 'import', 'require', 'default']; + return serverWebpackConfig; }; diff --git a/react_on_rails_pro/spec/dummy/package.json b/react_on_rails_pro/spec/dummy/package.json index a85f8492b0..a649cd0c8b 100644 --- a/react_on_rails_pro/spec/dummy/package.json +++ b/react_on_rails_pro/spec/dummy/package.json @@ -79,6 +79,7 @@ "@babel/preset-typescript": "^7.23.2", "@playwright/test": "^1.56.1", "@pmmmwh/react-refresh-webpack-plugin": "0.5.3", + "@swc/core": "^1.15.0", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.9", @@ -88,6 +89,7 @@ "jsdom": "^16.4.0", "pino-pretty": "^13.0.0", "preload-webpack-plugin": "^3.0.0-alpha.1", + "swc-loader": "^0.2.6", "typescript": "^5.2.2", "webpack-dev-server": "^4.7.3" }, diff --git a/react_on_rails_pro/spec/execjs-compatible-dummy/config/webpack/serverWebpackConfig.js b/react_on_rails_pro/spec/execjs-compatible-dummy/config/webpack/serverWebpackConfig.js index 9673f64a0a..749ed8fb23 100644 --- a/react_on_rails_pro/spec/execjs-compatible-dummy/config/webpack/serverWebpackConfig.js +++ b/react_on_rails_pro/spec/execjs-compatible-dummy/config/webpack/serverWebpackConfig.js @@ -48,6 +48,8 @@ const configureServer = () => { serverWebpackConfig.output = { filename: 'server-bundle.js', globalObject: 'this', + // Note: libraryTarget should only be 'commonjs2' when using Node renderer + // For ExecJS (default), leave it undefined to allow proper evaluation // If using the React on Rails Pro node server renderer, uncomment the next line // libraryTarget: 'commonjs2', path: config.outputPath, @@ -109,8 +111,18 @@ const configureServer = () => { // If using the default 'web', then libraries like Emotion and loadable-components // break with SSR. The fix is to use a node renderer and change the target. - // If using the React on Rails Pro node server renderer, uncomment the next line - // serverWebpackConfig.target = 'node' + // React 19 requires target: 'node' to properly handle Node.js built-in modules + serverWebpackConfig.target = 'node'; + + // React 19 Fix: Prevent webpack from resolving to react-server condition + // The server-bundle needs the full React with hooks for SSR, not the react-server build + // Explicitly set conditionNames without react-server + if (!serverWebpackConfig.resolve) { + serverWebpackConfig.resolve = {}; + } + // For target: 'node', webpack defaults to ['node', 'import', 'require', 'default'] + // We explicitly list them to ensure react-server is not included + serverWebpackConfig.resolve.conditionNames = ['node', 'import', 'require', 'default']; return serverWebpackConfig; }; diff --git a/react_on_rails_pro/spec/execjs-compatible-dummy/package.json b/react_on_rails_pro/spec/execjs-compatible-dummy/package.json index 2a73e44834..507b3d4c84 100644 --- a/react_on_rails_pro/spec/execjs-compatible-dummy/package.json +++ b/react_on_rails_pro/spec/execjs-compatible-dummy/package.json @@ -44,7 +44,9 @@ }, "devDependencies": { "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", + "@swc/core": "^1.15.0", "react-refresh": "^0.14.2", + "swc-loader": "^0.2.6", "webpack-dev-server": "4" } } diff --git a/spec/dummy/config/webpack/serverWebpackConfig.js b/spec/dummy/config/webpack/serverWebpackConfig.js index 33af3e9eb1..e163f54d0c 100644 --- a/spec/dummy/config/webpack/serverWebpackConfig.js +++ b/spec/dummy/config/webpack/serverWebpackConfig.js @@ -45,6 +45,8 @@ const configureServer = () => { serverWebpackConfig.output = { filename: 'server-bundle.js', globalObject: 'this', + // Note: libraryTarget should only be 'commonjs2' when using Node renderer + // For ExecJS (default), leave it undefined to allow proper evaluation // If using the React on Rails Pro node server renderer, uncomment the next line // libraryTarget: 'commonjs2', path: config.outputPath, @@ -107,8 +109,18 @@ const configureServer = () => { // If using the default 'web', then libraries like Emotion and loadable-components // break with SSR. The fix is to use a node renderer and change the target. - // If using the React on Rails Pro node server renderer, uncomment the next line - // serverWebpackConfig.target = 'node' + // React 19 requires target: 'node' to properly handle Node.js built-in modules + serverWebpackConfig.target = 'node'; + + // React 19 Fix: Prevent webpack from resolving to react-server condition + // The server-bundle needs the full React with hooks for SSR, not the react-server build + // Explicitly set conditionNames without react-server + if (!serverWebpackConfig.resolve) { + serverWebpackConfig.resolve = {}; + } + // For target: 'node', webpack defaults to ['node', 'import', 'require', 'default'] + // We explicitly list them to ensure react-server is not included + serverWebpackConfig.resolve.conditionNames = ['node', 'import', 'require', 'default']; return serverWebpackConfig; }; diff --git a/spec/dummy/package.json b/spec/dummy/package.json index 958cd2df5c..90ca6222cb 100644 --- a/spec/dummy/package.json +++ b/spec/dummy/package.json @@ -35,6 +35,7 @@ "@babel/preset-react": "^7.10.4", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.1", "@rescript/react": "^0.13.0", + "@swc/core": "^1.15.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/react-helmet": "^6.1.5", @@ -53,6 +54,7 @@ "sass-resources-loader": "^2.1.0", "shakapacker": "9.3.0", "style-loader": "^3.3.1", + "swc-loader": "^0.2.6", "terser-webpack-plugin": "5.3.1", "url-loader": "^4.0.0", "webpack": "5.72.0", diff --git a/spec/dummy/yarn.lock b/spec/dummy/yarn.lock index 861988c666..ed01f0e71a 100644 --- a/spec/dummy/yarn.lock +++ b/spec/dummy/yarn.lock @@ -1362,6 +1362,87 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@swc/core-darwin-arm64@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.0.tgz#158a0890fb2546b4d57b99234c1033e4a38b62e2" + integrity sha512-TBKWkbnShnEjlIbO4/gfsrIgAqHBVqgPWLbWmPdZ80bF393yJcLgkrb7bZEnJs6FCbSSuGwZv2rx1jDR2zo6YA== + +"@swc/core-darwin-x64@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.0.tgz#d03a71e60244f19ac921bf23c2cafc4122d76d8e" + integrity sha512-f5JKL1v1H56CIZc1pVn4RGPOfnWqPwmuHdpf4wesvXunF1Bx85YgcspW5YxwqG5J9g3nPU610UFuExJXVUzOiQ== + +"@swc/core-linux-arm-gnueabihf@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.0.tgz#fe978712a8924c0555c6b248ad3b57912ba123fb" + integrity sha512-duK6nG+WyuunnfsfiTUQdzC9Fk8cyDLqT9zyXvY2i2YgDu5+BH5W6wM5O4mDNCU5MocyB/SuF5YDF7XySnowiQ== + +"@swc/core-linux-arm64-gnu@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.0.tgz#a5dacdd857dec4ac2931820def17bc0e42c88ede" + integrity sha512-ITe9iDtTRXM98B91rvyPP6qDVbhUBnmA/j4UxrHlMQ0RlwpqTjfZYZkD0uclOxSZ6qIrOj/X5CaoJlDUuQ0+Cw== + +"@swc/core-linux-arm64-musl@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.0.tgz#243643a7d22c8e2f334046c1d76f342ad4369be9" + integrity sha512-Q5ldc2bzriuzYEoAuqJ9Vr3FyZhakk5hiwDbniZ8tlEXpbjBhbOleGf9/gkhLaouDnkNUEazFW9mtqwUTRdh7Q== + +"@swc/core-linux-x64-gnu@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.0.tgz#26936f55c916f65d33a4cf957c7573722f9eca54" + integrity sha512-pY4is+jEpOxlYCSnI+7N8Oxbap9TmTz5YT84tUvRTlOlTBwFAUlWFCX0FRwWJlsfP0TxbqhIe8dNNzlsEmJbXQ== + +"@swc/core-linux-x64-musl@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.0.tgz#a7164c11ac86ed99a1d5d8bef86ec0fbe6235f6c" + integrity sha512-zYEt5eT8y8RUpoe7t5pjpoOdGu+/gSTExj8PV86efhj6ugB3bPlj3Y85ogdW3WMVXr4NvwqvzdaYGCZfXzSyVg== + +"@swc/core-win32-arm64-msvc@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.0.tgz#645fe54564eab4224127672f2f4fe44876223af0" + integrity sha512-zC1rmOgFH5v2BCbByOazEqs0aRNpTdLRchDExfcCfgKgeaD+IdpUOqp7i3VG1YzkcnbuZjMlXfM0ugpt+CddoA== + +"@swc/core-win32-ia32-msvc@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.0.tgz#fd70c8c8b542a52a88cda758fb82569d52ea949a" + integrity sha512-7t9U9KwMwQblkdJIH+zX1V4q1o3o41i0HNO+VlnAHT5o+5qHJ963PHKJ/pX3P2UlZnBCY465orJuflAN4rAP9A== + +"@swc/core-win32-x64-msvc@1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.0.tgz#1d4f06078c7dbf757c537dd08740472694257198" + integrity sha512-VE0Zod5vcs8iMLT64m5QS1DlTMXJFI/qSgtMDRx8rtZrnjt6/9NW8XUaiPJuRu8GluEO1hmHoyf1qlbY19gGSQ== + +"@swc/core@^1.15.0": + version "1.15.0" + resolved "https://registry.npmjs.org/@swc/core/-/core-1.15.0.tgz#6ae4dbd5a164261ba799ccdf9eae3bbc61e112c2" + integrity sha512-8SnJV+JV0rYbfSiEiUvYOmf62E7QwsEG+aZueqSlKoxFt0pw333+bgZSQXGUV6etXU88nxur0afVMaINujBMSw== + dependencies: + "@swc/counter" "^0.1.3" + "@swc/types" "^0.1.25" + optionalDependencies: + "@swc/core-darwin-arm64" "1.15.0" + "@swc/core-darwin-x64" "1.15.0" + "@swc/core-linux-arm-gnueabihf" "1.15.0" + "@swc/core-linux-arm64-gnu" "1.15.0" + "@swc/core-linux-arm64-musl" "1.15.0" + "@swc/core-linux-x64-gnu" "1.15.0" + "@swc/core-linux-x64-musl" "1.15.0" + "@swc/core-win32-arm64-msvc" "1.15.0" + "@swc/core-win32-ia32-msvc" "1.15.0" + "@swc/core-win32-x64-msvc" "1.15.0" + +"@swc/counter@^0.1.3": + version "0.1.3" + resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" + integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== + +"@swc/types@^0.1.25": + version "0.1.25" + resolved "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz#b517b2a60feb37dd933e542d93093719e4cf1078" + integrity sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g== + dependencies: + "@swc/counter" "^0.1.3" + "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -5968,6 +6049,13 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swc-loader@^0.2.6: + version "0.2.6" + resolved "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz#bf0cba8eeff34bb19620ead81d1277fefaec6bc8" + integrity sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg== + dependencies: + "@swc/counter" "^0.1.3" + symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"