Skip to content

Commit 35ceeca

Browse files
committed
wip: prerender
1 parent 79c5578 commit 35ceeca

File tree

4 files changed

+98
-3
lines changed

4 files changed

+98
-3
lines changed

packages/plugin-rsc/examples/react-router/nitro.ts

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ import {
33
copyPublicAssets,
44
createNitro,
55
prepare,
6+
type $Fetch,
7+
type Nitro,
68
type NitroConfig,
79
} from 'nitropack'
810
import type { PresetName } from 'nitropack/presets'
11+
import path from 'node:path'
12+
import { pathToFileURL } from 'node:url'
913
import type { Plugin, ViteBuilder } from 'vite'
14+
import { joinURL, withBase, withoutBase } from 'ufo'
15+
import fsp from 'node:fs/promises'
1016

1117
// Using Nitro as post-build to target deployment platform. Inspired by Tanstack Start's approach.
1218
// https://github.com/TanStack/router/blob/5fd079e482b1252b8b11a936f1524a0dee368cae/packages/start-plugin-core/src/nitro-plugin/plugin.ts
@@ -18,6 +24,7 @@ type NitroPluginOptions = {
1824
preset: PresetName
1925
clientDir: string
2026
serverEntry: string
27+
prerender?: string[]
2128
}
2229

2330
export function nitroPlugin(nitroPluginOptions: NitroPluginOptions): Plugin[] {
@@ -121,10 +128,93 @@ export default defineEventHandler((event) => handler(toWebRequest(event)))
121128
const nitro = await createNitro(nitroConfig)
122129
await prepare(nitro)
123130
await copyPublicAssets(nitro)
124-
await prerender()
131+
if (nitroPluginOptions.prerender?.length) {
132+
await prerender(nitro, nitroPluginOptions.prerender)
133+
}
125134
await build(nitro)
126135
await nitro.close()
136+
}
137+
138+
// TODO
139+
// https://github.com/TanStack/router/blob/5fd079e482b1252b8b11a936f1524a0dee368cae/packages/start-plugin-core/src/nitro-plugin/prerender.ts#L53
140+
// https://github.com/nitrojs/nitro/blob/c468de271cff8d56361c3b09ea1071ed545a550f/src/prerender/prerender.ts#L62-L74
141+
async function prerender(nitro: Nitro, pages: string[]) {
142+
const nodeNitro = await createNitro({
143+
...nitro.options._config,
144+
preset: 'nitro-prerender',
145+
// logLevel: 0,
146+
output: {
147+
dir: 'dist/nitro/prerender',
148+
// serverDir: path.resolve(prerenderOutputDir, 'server'),
149+
// publicDir: path.resolve(prerenderOutputDir, 'public'),
150+
},
151+
})
152+
await build(nodeNitro)
153+
154+
const serverEntrypoint = pathToFileURL(
155+
path.resolve(nodeNitro.options.output.serverDir, 'index.mjs'),
156+
).toString()
157+
158+
const { closePrerenderer, localFetch } = (await import(serverEntrypoint)) as {
159+
closePrerenderer: () => void
160+
localFetch: $Fetch
161+
}
162+
163+
for (const page of pages) {
164+
await prerenderPage({ path: page })
165+
}
166+
closePrerenderer()
167+
await nodeNitro.close()
168+
169+
async function prerenderPage(page: { path: string }) {
170+
const encodedRoute = encodeURI(page.path)
171+
172+
const res = await localFetch<Response>(
173+
withBase(encodedRoute, nodeNitro.options.baseURL),
174+
{
175+
headers: { 'x-nitro-prerender': encodedRoute },
176+
},
177+
)
178+
179+
if (!res.ok) {
180+
throw new Error(`Failed to fetch ${page.path}: ${res.statusText}`, {
181+
cause: res,
182+
})
183+
}
184+
185+
// const cleanPagePath = (prerenderOptions.outputPath || page.path).split(
186+
// /[?#]/,
187+
// )[0]!
188+
const cleanPagePath = page.path
189+
190+
// Guess route type and populate fileName
191+
const contentType = res.headers.get('content-type') || ''
192+
const isImplicitHTML =
193+
!cleanPagePath.endsWith('.html') && contentType.includes('html')
194+
// &&
195+
// !JsonSigRx.test(dataBuff.subarray(0, 32).toString('utf8'))
196+
const routeWithIndex = cleanPagePath.endsWith('/')
197+
? cleanPagePath + 'index'
198+
: cleanPagePath
199+
200+
const htmlPath = cleanPagePath.endsWith('/')
201+
? // || prerenderOptions.autoSubfolderIndex
202+
joinURL(cleanPagePath, 'index.html')
203+
: cleanPagePath + '.html'
127204

128-
// TODO
129-
async function prerender() {}
205+
const filename = withoutBase(
206+
isImplicitHTML ? htmlPath : routeWithIndex,
207+
nitro.options.baseURL,
208+
)
209+
210+
const html = await res.text()
211+
212+
const filepath = path.join(nitro.options.output.publicDir, filename)
213+
214+
await fsp.mkdir(path.dirname(filepath), {
215+
recursive: true,
216+
})
217+
218+
await fsp.writeFile(filepath, html)
219+
}
130220
}

packages/plugin-rsc/examples/react-router/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"h3": "^1.15.3",
3030
"nitropack": "^2.11.13",
3131
"tailwindcss": "^4.1.11",
32+
"ufo": "^1.6.1",
3233
"vite": "^7.0.2",
3334
"vite-plugin-inspect": "^11.3.0",
3435
"wrangler": "^4.23.0"

packages/plugin-rsc/examples/react-router/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default defineConfig({
3535
// - builder.environments.client.config.build.outDir
3636
clientDir: path.resolve('./dist/client'),
3737
serverEntry: path.resolve('./dist/ssr/index.js'),
38+
// prerender: ['/'],
3839
}),
3940
],
4041
optimizeDeps: {

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)