feat: update rendering from renderToString to renderToReadableStream (#233)

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
This commit is contained in:
Valerio Ageno
2024-12-18 21:58:19 +01:00
committed by GitHub
parent 21ff3c8655
commit 3e4e7ff1ab
5 changed files with 98 additions and 7 deletions
+3 -1
View File
@@ -101,6 +101,7 @@
"@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",
@@ -109,7 +110,8 @@
"tuono-fs-router-vite-plugin": "workspace:*",
"tuono-lazy-fn-vite-plugin": "workspace:*",
"tuono-router": "workspace:*",
"vite": "^5.2.11"
"vite": "^5.2.11",
"web-streams-polyfill": "^4.0.0"
},
"devDependencies": {
"@types/babel-traverse": "^6.25.10",
+12 -1
View File
@@ -1,6 +1,7 @@
import type { InlineConfig } from 'vite'
import type { InlineConfig, Plugin } from 'vite'
import { build, createServer, mergeConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import inject from '@rollup/plugin-inject'
import ViteFsRouter from 'tuono-fs-router-vite-plugin'
import { LazyLoadingPlugin } from 'tuono-lazy-fn-vite-plugin'
@@ -9,6 +10,14 @@ import type { TuonoConfig } from '../config'
import { loadConfig, blockingAsync } from './utils'
const VITE_PORT = 3001
const VITE_SSR_PLUGINS: Array<Plugin> = [
{
enforce: 'post',
...inject({
ReadableStream: ['web-streams-polyfill', 'ReadableStream'],
}),
},
]
/**
* From a given {@link TuonoConfig} return a `vite` "mergeable" {@link InlineConfig}
@@ -65,6 +74,7 @@ const developmentSSRBundle = (): void => {
mergeConfig<InlineConfig, InlineConfig>(
createBaseViteConfigFromTuonoConfig(config),
{
plugins: VITE_SSR_PLUGINS,
build: {
ssr: true,
minify: false,
@@ -143,6 +153,7 @@ const buildProd = (): void => {
mergeConfig<InlineConfig, InlineConfig>(
createBaseViteConfigFromTuonoConfig(config),
{
plugins: VITE_SSR_PLUGINS,
build: {
ssr: true,
minify: true,
+16 -5
View File
@@ -1,11 +1,15 @@
import 'fast-text-encoding' // Mandatory for React18
import type { ReadableStream } from 'node:stream/web'
import * as React from 'react'
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import { renderToStaticMarkup, renderToReadableStream } from 'react-dom/server'
import type { HelmetServerState } from 'react-helmet-async'
import { HelmetProvider } from 'react-helmet-async'
import { RouterProvider, createRouter } from 'tuono-router'
import type { createRoute } from 'tuono-router'
import { streamToString } from './utils'
type RouteTree = ReturnType<typeof createRoute>
type Mode = 'Dev' | 'Prod'
@@ -37,7 +41,7 @@ function generateJsScripts(jsBundles: Array<string>, mode: Mode): string {
}
export function serverSideRendering(routeTree: RouteTree) {
return function render(payload: string | undefined): string {
return async function render(payload: string | undefined): Promise<string> {
const serverProps = (payload ? JSON.parse(payload) : {}) as Record<
string,
unknown
@@ -48,14 +52,21 @@ export function serverSideRendering(routeTree: RouteTree) {
const cssBundles = serverProps.cssBundles as Array<string>
const router = createRouter({ routeTree }) // Render the app
const helmetContext = {}
const app = renderToString(
const helmetContext = {} as { helmet: HelmetServerState }
const stream = await renderToReadableStream(
<HelmetProvider context={helmetContext}>
<RouterProvider router={router} serverProps={serverProps as never} />
</HelmetProvider>,
)
const { helmet } = helmetContext as { helmet: HelmetServerState }
await stream.allReady
const { helmet } = helmetContext
const app = await streamToString(
// ReadableStream should be implemented in node)
stream as unknown as ReadableStream<Uint8Array>,
)
return `<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
+38
View File
@@ -0,0 +1,38 @@
// react ReadableStream type is an empty interface so we are using the one from
// node which match the runtime value
import type { ReadableStream } from 'node:stream/web'
function concatArrayBuffers(chunks: Array<Uint8Array>): Uint8Array {
const result = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0))
let offset = 0
for (const chunk of chunks) {
result.set(chunk, offset)
offset += chunk.length
}
return result
}
async function streamToArrayBuffer(
stream: ReadableStream<Uint8Array>,
): Promise<Uint8Array> {
const chunks: Array<Uint8Array> = []
for await (const chunk of stream) {
chunks.push(chunk)
}
return concatArrayBuffers(chunks)
}
/**
* This function awaits for the whole stream before returning the string.
*
* NOTE: we should improve the bond between the custom V8 runtime and the
* renderToReadableStream React function to return a stream directly to the client.
*/
export async function streamToString(
stream: ReadableStream<Uint8Array>,
): Promise<string> {
const buffer = await streamToArrayBuffer(stream)
return new TextDecoder().decode(buffer)
}
+29
View File
@@ -294,6 +294,9 @@ importers:
'@babel/types':
specifier: ^7.24.0
version: 7.26.0
'@rollup/plugin-inject':
specifier: ^5.0.5
version: 5.0.5(rollup@4.25.0)
'@types/babel__core':
specifier: ^7.20.5
version: 7.20.5
@@ -327,6 +330,9 @@ importers:
vite:
specifier: ^5.2.11
version: 5.4.11(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49))
web-streams-polyfill:
specifier: ^4.0.0
version: 4.0.0
devDependencies:
'@types/babel-traverse':
specifier: ^6.25.10
@@ -799,6 +805,15 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
'@rollup/plugin-inject@5.0.5':
resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/pluginutils@5.1.3':
resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==}
engines: {node: '>=14.0.0'}
@@ -3423,6 +3438,10 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
web-streams-polyfill@4.0.0:
resolution: {integrity: sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==}
engines: {node: '>= 8'}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -4031,6 +4050,14 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
'@rollup/plugin-inject@5.0.5(rollup@4.25.0)':
dependencies:
'@rollup/pluginutils': 5.1.3(rollup@4.25.0)
estree-walker: 2.0.2
magic-string: 0.30.14
optionalDependencies:
rollup: 4.25.0
'@rollup/pluginutils@5.1.3(rollup@4.25.0)':
dependencies:
'@types/estree': 1.0.6
@@ -7276,6 +7303,8 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
web-streams-polyfill@4.0.0: {}
webidl-conversions@7.0.0: {}
whatwg-encoding@3.1.1: