From 592f402ffd06357e5a490921f9cfa7eb98ffb386 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:00:44 +0100 Subject: [PATCH] feat: add support for react 19 (#264) Co-authored-by: Valerio Ageno --- examples/tuono-app/package.json | 10 +- examples/tuono-app/src/routes/__root.tsx | 8 +- examples/tuono-tutorial/package.json | 8 +- examples/tuono-tutorial/src/routes/__root.tsx | 8 +- examples/tuono-tutorial/src/routes/index.tsx | 30 +- .../src/routes/pokemons/[pokemon].tsx | 7 +- examples/tuono-tutorial/src/styles/global.css | 1 + examples/with-mdx/package.json | 8 +- examples/with-mdx/src/routes/__root.tsx | 10 +- package.json | 2 +- .../tuono-fs-router-vite-plugin/package.json | 2 +- .../tuono-lazy-fn-vite-plugin/package.json | 2 +- packages/tuono-router/package.json | 14 +- .../src/components/RouteMatch.tsx | 4 +- .../src/components/RouterProvider.tsx | 15 +- packages/tuono/package.json | 28 +- packages/tuono/src/hydration/index.tsx | 10 +- packages/tuono/src/index.ts | 2 - packages/tuono/src/react-meta-tags.d.ts | 2 - .../tuono/src/ssr/components/DevResources.tsx | 21 + .../src/ssr/components/ProdResources.tsx | 26 ++ packages/tuono/src/ssr/server.tsx | 117 +++-- packages/tuono/src/ssr/types.ts | 1 + pnpm-lock.yaml | 419 ++++++------------ 24 files changed, 329 insertions(+), 426 deletions(-) delete mode 100644 packages/tuono/src/react-meta-tags.d.ts create mode 100644 packages/tuono/src/ssr/components/DevResources.tsx create mode 100644 packages/tuono/src/ssr/components/ProdResources.tsx create mode 100644 packages/tuono/src/ssr/types.ts diff --git a/examples/tuono-app/package.json b/examples/tuono-app/package.json index 1596dea5..96d94478 100644 --- a/examples/tuono-app/package.json +++ b/examples/tuono-app/package.json @@ -3,13 +3,13 @@ "description": "Basic tuono application", "version": "0.0.1", "dependencies": { - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "tuono": "link:../../packages/tuono" }, "devDependencies": { - "@types/react": "^18.3.13", - "@types/react-dom": "^18.3.0", - "typescript": "^5.6.3" + "@types/react": "19.0.2", + "@types/react-dom": "19.0.2", + "typescript": "5.6.3" } } diff --git a/examples/tuono-app/src/routes/__root.tsx b/examples/tuono-app/src/routes/__root.tsx index 2a9d1425..8218e6fd 100644 --- a/examples/tuono-app/src/routes/__root.tsx +++ b/examples/tuono-app/src/routes/__root.tsx @@ -5,5 +5,11 @@ interface RootRouteProps { } export default function RootRoute({ children }: RootRouteProps): JSX.Element { - return
{children}
+ return ( + + +
{children}
+ + + ) } diff --git a/examples/tuono-tutorial/package.json b/examples/tuono-tutorial/package.json index ef8796a1..35be0805 100644 --- a/examples/tuono-tutorial/package.json +++ b/examples/tuono-tutorial/package.json @@ -3,13 +3,13 @@ "description": "The basic tutorial for tuono applications", "version": "0.0.1", "dependencies": { - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "tuono": "link:../../packages/tuono" }, "devDependencies": { - "@types/react": "^18.3.13", - "@types/react-dom": "^18.3.0", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", "typescript": "^5.6.3" } } diff --git a/examples/tuono-tutorial/src/routes/__root.tsx b/examples/tuono-tutorial/src/routes/__root.tsx index 2a9d1425..8218e6fd 100644 --- a/examples/tuono-tutorial/src/routes/__root.tsx +++ b/examples/tuono-tutorial/src/routes/__root.tsx @@ -5,5 +5,11 @@ interface RootRouteProps { } export default function RootRoute({ children }: RootRouteProps): JSX.Element { - return
{children}
+ return ( + + +
{children}
+ + + ) } diff --git a/examples/tuono-tutorial/src/routes/index.tsx b/examples/tuono-tutorial/src/routes/index.tsx index 79d26d55..377189f3 100644 --- a/examples/tuono-tutorial/src/routes/index.tsx +++ b/examples/tuono-tutorial/src/routes/index.tsx @@ -1,6 +1,6 @@ // src/routes/index.tsx import type { JSX } from 'react' -import { Head, type TuonoProps } from 'tuono' +import type { TuonoProps } from 'tuono' import PokemonLink from '@/components/PokemonLink' @@ -15,20 +15,25 @@ interface IndexProps { export default function IndexPage({ data, }: TuonoProps): JSX.Element | null { - if (!data?.results) { - return null - } + if (!data?.results) return null return ( <> - - Tuono tutorial - + Tuono tutorial +
- + Crates - + Npm
@@ -43,9 +48,10 @@ export default function IndexPage({ ) diff --git a/examples/tuono-tutorial/src/routes/pokemons/[pokemon].tsx b/examples/tuono-tutorial/src/routes/pokemons/[pokemon].tsx index cfb2ea72..847cc68b 100644 --- a/examples/tuono-tutorial/src/routes/pokemons/[pokemon].tsx +++ b/examples/tuono-tutorial/src/routes/pokemons/[pokemon].tsx @@ -1,5 +1,5 @@ import type { JSX } from 'react' -import { Head, type TuonoProps } from 'tuono' +import type { TuonoProps } from 'tuono' import PokemonView from '@/components/PokemonView' @@ -15,9 +15,8 @@ export default function PokemonPage({ }: TuonoProps): JSX.Element { return ( <> - - {`Pokemon: ${data?.name}`} - + {`Pokemon: ${data?.name ?? ''}`} + ) diff --git a/examples/tuono-tutorial/src/styles/global.css b/examples/tuono-tutorial/src/styles/global.css index 84fc0842..af1e31e3 100644 --- a/examples/tuono-tutorial/src/styles/global.css +++ b/examples/tuono-tutorial/src/styles/global.css @@ -4,6 +4,7 @@ 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 93b7bf92..1fb9302b 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -4,14 +4,14 @@ "version": "0.0.1", "dependencies": { "@mdx-js/react": "3.1.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "tuono": "link:../../packages/tuono" }, "devDependencies": { "@mdx-js/rollup": "3.1.0", - "@types/react": "18.3.18", - "@types/react-dom": "18.3.5", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", "typescript": "^5.6.3" } } diff --git a/examples/with-mdx/src/routes/__root.tsx b/examples/with-mdx/src/routes/__root.tsx index f85690e2..7599d3bf 100644 --- a/examples/with-mdx/src/routes/__root.tsx +++ b/examples/with-mdx/src/routes/__root.tsx @@ -7,8 +7,12 @@ interface RootRouteProps { export default function RootRoute({ children }: RootRouteProps): JSX.Element { return ( -
- {children} -
+ + +
+ {children} +
+ + ) } diff --git a/package.json b/package.json index 28baccf5..1b0b0323 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "license": "MIT", "devDependencies": { "@eslint/js": "9.15.0", - "@types/node": "22.10.0", + "@types/node": "22.10.3", "eslint": "9.15.0", "eslint-import-resolver-typescript": "3.6.3", "eslint-plugin-import": "2.31.0", diff --git a/packages/tuono-fs-router-vite-plugin/package.json b/packages/tuono-fs-router-vite-plugin/package.json index f4a75d28..3d4d4e4b 100644 --- a/packages/tuono-fs-router-vite-plugin/package.json +++ b/packages/tuono-fs-router-vite-plugin/package.json @@ -52,6 +52,6 @@ "devDependencies": { "@tanstack/config": "0.7.13", "@types/babel__core": "^7.20.5", - "vitest": "^2.0.0" + "vitest": "2.1.8" } } diff --git a/packages/tuono-lazy-fn-vite-plugin/package.json b/packages/tuono-lazy-fn-vite-plugin/package.json index 93695902..4049f49a 100644 --- a/packages/tuono-lazy-fn-vite-plugin/package.json +++ b/packages/tuono-lazy-fn-vite-plugin/package.json @@ -52,6 +52,6 @@ "@tanstack/config": "0.7.13", "@types/babel__core": "^7.20.5", "prettier": "^3.2.4", - "vitest": "^2.0.0" + "vitest": "2.1.8" } } diff --git a/packages/tuono-router/package.json b/packages/tuono-router/package.json index ea48fa83..d46323d6 100644 --- a/packages/tuono-router/package.json +++ b/packages/tuono-router/package.json @@ -44,20 +44,20 @@ "./package.json": "./package.json" }, "peerDependencies": { - "react": ">=16.3.0" + "react": ">=19.0.0" }, "dependencies": { "react-intersection-observer": "^9.13.0" }, "devDependencies": { "@tanstack/config": "0.7.13", - "@testing-library/jest-dom": "^6.6.0", - "@testing-library/react": "^16.0.0", - "@types/react": "18.3.18", + "@testing-library/jest-dom": "6.6.3", + "@testing-library/react": "16.1.0", + "@types/react": "19.0.2", "@vitejs/plugin-react-swc": "3.7.2", - "react": "18.3.1", - "jsdom": "^25.0.0", + "jsdom": "25.0.1", + "react": "19.0.0", "vite": "5.4.11", - "vitest": "^2.0.0" + "vitest": "2.1.8" } } diff --git a/packages/tuono-router/src/components/RouteMatch.tsx b/packages/tuono-router/src/components/RouteMatch.tsx index 54b65ba6..48ef522e 100644 --- a/packages/tuono-router/src/components/RouteMatch.tsx +++ b/packages/tuono-router/src/components/RouteMatch.tsx @@ -25,7 +25,9 @@ export const RouteMatch = ({ return ( - + + + ) } diff --git a/packages/tuono-router/src/components/RouterProvider.tsx b/packages/tuono-router/src/components/RouterProvider.tsx index 92b62daa..eb9fa46f 100644 --- a/packages/tuono-router/src/components/RouterProvider.tsx +++ b/packages/tuono-router/src/components/RouterProvider.tsx @@ -1,5 +1,4 @@ import type { JSX } from 'react' -import { Suspense } from 'react' import type { ServerProps } from '../types' import type { Router } from '../router' @@ -20,13 +19,11 @@ export function RouterProvider({ serverProps, }: RouterProviderProps): JSX.Element { return ( - - - - - + + + ) } diff --git a/packages/tuono/package.json b/packages/tuono/package.json index 20fa2bd5..97b62303 100644 --- a/packages/tuono/package.json +++ b/packages/tuono/package.json @@ -88,25 +88,16 @@ "bin/**" ], "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" + "react": ">=19.0.0", + "react-dom": ">=19.0.0" }, "dependencies": { "@babel/core": "^7.24.4", - "@babel/generator": "^7.23.6", "@babel/plugin-syntax-jsx": "^7.24.1", "@babel/plugin-syntax-typescript": "^7.24.1", - "@babel/plugin-transform-react-jsx": "^7.23.4", - "@babel/plugin-transform-typescript": "^7.24.1", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", "@rollup/plugin-inject": "^5.0.5", - "@types/babel__core": "^7.20.5", - "@types/node": "^22.0.0", "@vitejs/plugin-react-swc": "^3.7.0", "fast-text-encoding": "^1.0.6", - "react-helmet-async": "^2.0.5", "tuono-fs-router-vite-plugin": "workspace:*", "tuono-lazy-fn-vite-plugin": "workspace:*", "tuono-router": "workspace:*", @@ -115,13 +106,14 @@ }, "devDependencies": { "@tanstack/config": "0.7.13", - "@types/babel-traverse": "^6.25.10", - "@types/babel__traverse": "^7.20.6", - "@types/react": "18.3.18", - "@types/react-dom": "18.3.5", - "react": "18.3.1", - "react-dom": "18.3.1", - "vitest": "^2.0.0" + "@types/babel__core": "7.20.5", + "@types/babel__traverse": "7.20.6", + "@types/node": "22.10.3", + "@types/react": "19.0.2", + "@types/react-dom": "19.0.2", + "react": "19.0.0", + "react-dom": "19.0.0", + "vitest": "2.1.8" }, "sideEffects": false, "keywords": [ diff --git a/packages/tuono/src/hydration/index.tsx b/packages/tuono/src/hydration/index.tsx index 0193bb20..ba1fa8a3 100644 --- a/packages/tuono/src/hydration/index.tsx +++ b/packages/tuono/src/hydration/index.tsx @@ -2,7 +2,6 @@ import React from 'react' import { hydrateRoot } from 'react-dom/client' import { RouterProvider, createRouter } from 'tuono-router' import type { createRoute } from 'tuono-router' -import { HelmetProvider } from 'react-helmet-async' type RouteTree = ReturnType @@ -10,15 +9,10 @@ export function hydrate(routeTree: RouteTree): void { // Create a new router instance const router = createRouter({ routeTree }) - // eslint-disable-next-line - const rootElement = document.getElementById('__tuono')! - hydrateRoot( - rootElement, + document, - - - + , ) } diff --git a/packages/tuono/src/index.ts b/packages/tuono/src/index.ts index ea745dbe..7f8aea32 100644 --- a/packages/tuono/src/index.ts +++ b/packages/tuono/src/index.ts @@ -1,5 +1,3 @@ -export { Helmet as Head } from 'react-helmet-async' - export { createRoute, createRootRoute, diff --git a/packages/tuono/src/react-meta-tags.d.ts b/packages/tuono/src/react-meta-tags.d.ts deleted file mode 100644 index 85eaf7fc..00000000 --- a/packages/tuono/src/react-meta-tags.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module 'react-meta-tags' -declare module 'react-meta-tags/server' diff --git a/packages/tuono/src/ssr/components/DevResources.tsx b/packages/tuono/src/ssr/components/DevResources.tsx new file mode 100644 index 00000000..c5a7f485 --- /dev/null +++ b/packages/tuono/src/ssr/components/DevResources.tsx @@ -0,0 +1,21 @@ +import type { JSX } from 'react' + +const TUONO_DEV_SERVER_PORT = 3000 +const VITE_PROXY_PATH = '/vite-server' +const SCRIPT_BASE_URL = `http://localhost:${TUONO_DEV_SERVER_PORT}${VITE_PROXY_PATH}` + +export const DevResources = (): JSX.Element => ( + <> + + + + +) diff --git a/packages/tuono/src/ssr/components/ProdResources.tsx b/packages/tuono/src/ssr/components/ProdResources.tsx new file mode 100644 index 00000000..fdb788b8 --- /dev/null +++ b/packages/tuono/src/ssr/components/ProdResources.tsx @@ -0,0 +1,26 @@ +import type { JSX } from 'react' + +interface ProdResourcesProps { + cssBundles: Array + jsBundles: Array +} + +export const ProdResources = ({ + cssBundles, + jsBundles, +}: ProdResourcesProps): JSX.Element => ( + <> + {cssBundles.map((cssHref) => ( + + ))} + + {jsBundles.map((scriptSrc) => ( + + ))} + +) diff --git a/packages/tuono/src/ssr/server.tsx b/packages/tuono/src/ssr/server.tsx index ec82b9ad..253ed004 100644 --- a/packages/tuono/src/ssr/server.tsx +++ b/packages/tuono/src/ssr/server.tsx @@ -1,43 +1,55 @@ -import 'fast-text-encoding' // Mandatory for React18 +// #region POLYFILLS +/** + * Tuono internally uses a V8 JS engine that implements very few + * browser/node/deno APIs in order to make it super fast and + * share it within a multi thread runtime. + * + * While this is the reason of its speed, server side rendering + * requires some JS APIs that need to be polyfilled. + * + * We basically have three ways to polyfill APIs: + * 1. Create them with rust and expose them directly through the V8 engine to + * the JS source. + * 2. Polyfill them at the beginning of the JS source + * (what we are doing here) + * 3. Inject them via rollup-inject plugin, when needed + * + * Q: Why not all the libraries can be just injected with rollup-inject? + * A: Leaving to rollup the duty of linking them can cause to declare them after their usage. + * The following APIs are JS classes, and are not hoisted, hence this might + * cause ReferenceError(s). + * + * The best solution is to create these polyfills within the rust environment + * and share the classes in the JS scope by passing them through the V8 engine + * (best for speed and code quality). + * + * This function might be a good entry point for adding such polyfills + * https://docs.rs/ssr_rs/latest/ssr_rs/struct.Ssr.html#method.add_global_fn + */ +import 'fast-text-encoding' + +/* eslint-disable import/order, import/newline-after-import */ +import { MessageChannelPolyfill } from './polyfills/MessageChannel' +;(function ( + scope: Partial> = {}, +): void { + scope['MessageChannel'] = scope['MessageChannel'] ?? MessageChannelPolyfill +})(this) +/* eslint-enable import/order, import/newline-after-import */ +// #endregion POLYFILLS + import type { ReadableStream } from 'node:stream/web' -import { renderToStaticMarkup, renderToReadableStream } from 'react-dom/server' -import type { HelmetServerState } from 'react-helmet-async' -import { HelmetProvider } from 'react-helmet-async' +import { renderToReadableStream } from 'react-dom/server' import { RouterProvider, createRouter } from 'tuono-router' import type { createRoute } from 'tuono-router' +import { DevResources } from './components/DevResources' +import { ProdResources } from './components/ProdResources' +import type { Mode } from './types' import { streamToString } from './utils' type RouteTree = ReturnType -type Mode = 'Dev' | 'Prod' - -const TUONO_DEV_SERVER_PORT = 3000 -const VITE_PROXY_PATH = '/vite-server' - -const VITE_DEV_AND_HMR = ` - -` - -function generateCssLinks(cssBundles: Array, mode: Mode): string { - if (mode === 'Dev') return '' - return cssBundles.reduce((acc, value) => { - return acc + `` - }, '') -} - -function generateJsScripts(jsBundles: Array, mode: Mode): string { - if (mode === 'Dev') return '' - return jsBundles.reduce((acc, value) => { - return acc + `` - }, '') -} export function serverSideRendering(routeTree: RouteTree) { return async function render(payload: string | undefined): Promise { @@ -51,45 +63,24 @@ export function serverSideRendering(routeTree: RouteTree) { const cssBundles = serverProps.cssBundles as Array const router = createRouter({ routeTree }) // Render the app - const helmetContext = {} as { helmet: HelmetServerState } const stream = await renderToReadableStream( - + <> - , + + {mode === 'Dev' && } + {mode === 'Prod' && ( + + )} + + + , ) await stream.allReady - const { helmet } = helmetContext - - const app = await streamToString( + return await streamToString( // ReadableStream should be implemented in node) stream as unknown as ReadableStream, ) - - return ` - - - ${helmet.title.toString()} - ${helmet.priority.toString()} - ${helmet.meta.toString()} - ${helmet.link.toString()} - ${helmet.script.toString()} - ${generateCssLinks(cssBundles, mode)} - - -
${app}
- ${renderToStaticMarkup( -