Skip to content

Commit

Permalink
feat: update rendering from renderToString to `renderToReadableStre…
Browse files Browse the repository at this point in the history
…am` (#233)

Co-authored-by: Marco Pasqualetti <[email protected]>
  • Loading branch information
Valerioageno and marcalexiei authored Dec 18, 2024
1 parent 21ff3c8 commit 3e4e7ff
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 7 deletions.
4 changes: 3 additions & 1 deletion packages/tuono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
13 changes: 12 additions & 1 deletion packages/tuono/src/build/index.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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}
Expand Down Expand Up @@ -65,6 +74,7 @@ const developmentSSRBundle = (): void => {
mergeConfig<InlineConfig, InlineConfig>(
createBaseViteConfigFromTuonoConfig(config),
{
plugins: VITE_SSR_PLUGINS,
build: {
ssr: true,
minify: false,
Expand Down Expand Up @@ -143,6 +153,7 @@ const buildProd = (): void => {
mergeConfig<InlineConfig, InlineConfig>(
createBaseViteConfigFromTuonoConfig(config),
{
plugins: VITE_SSR_PLUGINS,
build: {
ssr: true,
minify: true,
Expand Down
21 changes: 16 additions & 5 deletions packages/tuono/src/ssr/index.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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
Expand All @@ -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()}>
Expand Down
38 changes: 38 additions & 0 deletions packages/tuono/src/ssr/utils.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3e4e7ff

Please sign in to comment.