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)
}