mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
feat: update rendering from renderToString to renderToReadableStream (#233)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user