Skip to content

Commit ca09453

Browse files
committed
use vite preview
1 parent 14f3b45 commit ca09453

File tree

3 files changed

+115
-13
lines changed

3 files changed

+115
-13
lines changed

packages/start-plugin-core/src/plugin.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -375,28 +375,33 @@ export function TanStackStartVitePluginCore(
375375
},
376376
},
377377
},
378-
// Nitro module plugin - runs prerendering after Nitro build
378+
// Nitro module plugin - runs prerendering after Nitro build using vite preview
379379
{
380380
name: 'tanstack-start-core:nitro-prerender',
381381
nitro: {
382382
name: 'tanstack-start-prerender',
383383
setup(nitro: any) {
384384
nitro.hooks.hook('compiled', async () => {
385-
const { startConfig } = getConfig()
385+
const { startConfig, resolvedStartConfig } = getConfig()
386386
if (!startConfig.prerender?.enabled && !startConfig.spa?.enabled) {
387387
return
388388
}
389389

390-
const { postServerBuildForNitro } = await import(
391-
'./post-server-build'
392-
)
390+
// Write nitro.json before calling vite.preview() since Nitro's
391+
// configurePreviewServer hook requires it to exist. The 'compiled'
392+
// hook runs before Nitro writes the build info, so we need to
393+
// write a minimal version ourselves.
394+
const { writeNitroBuildInfo, postServerBuildForNitro } =
395+
await import('./post-server-build')
396+
await writeNitroBuildInfo({
397+
outputDir: nitro.options.output.dir,
398+
preset: nitro.options.preset,
399+
})
400+
393401
await postServerBuildForNitro({
394402
startConfig,
395403
outputDir: nitro.options.output.publicDir,
396-
nitroServerPath: join(
397-
nitro.options.output.serverDir,
398-
'index.mjs',
399-
),
404+
configFile: resolvedStartConfig.viteConfigFile,
400405
})
401406
})
402407
},

packages/start-plugin-core/src/post-server-build.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,42 @@
1+
import { promises as fsp } from 'node:fs'
2+
import path from 'pathe'
13
import { HEADERS } from '@tanstack/start-server-core'
24
import { buildSitemap } from './build-sitemap'
35
import { VITE_ENVIRONMENT_NAMES } from './constants'
46
import { prerender } from './prerender'
7+
import { createLogger } from './utils'
58
import type { TanStackStartOutputConfig } from './schema'
69
import type { ViteBuilder } from 'vite'
710

11+
/**
12+
* Write a minimal nitro.json file for vite.preview() to work with Nitro's
13+
* configurePreviewServer hook. This is needed because the 'compiled' hook
14+
* runs before Nitro writes its build info.
15+
*/
16+
export async function writeNitroBuildInfo({
17+
outputDir,
18+
preset,
19+
}: {
20+
outputDir: string
21+
preset: string
22+
}) {
23+
const logger = createLogger('prerender')
24+
logger.info('Writing nitro.json for vite.preview()...')
25+
26+
const buildInfo = {
27+
date: new Date().toJSON(),
28+
preset,
29+
framework: { name: 'tanstack-start' },
30+
versions: {},
31+
commands: {
32+
preview: `node ${path.join(outputDir, 'server/index.mjs')}`,
33+
},
34+
}
35+
36+
const buildInfoPath = path.join(outputDir, 'nitro.json')
37+
await fsp.writeFile(buildInfoPath, JSON.stringify(buildInfo, null, 2))
38+
}
39+
840
function setupPrerenderConfig(startConfig: TanStackStartOutputConfig) {
941
if (startConfig.prerender?.enabled !== false) {
1042
startConfig.prerender = {
@@ -72,19 +104,19 @@ export async function postServerBuild({
72104
export async function postServerBuildForNitro({
73105
startConfig,
74106
outputDir,
75-
nitroServerPath,
107+
configFile,
76108
}: {
77109
startConfig: TanStackStartOutputConfig
78110
outputDir: string
79-
nitroServerPath: string
111+
configFile?: string
80112
}) {
81113
setupPrerenderConfig(startConfig)
82114

83115
if (startConfig.prerender?.enabled) {
84116
await prerender({
85117
startConfig,
86118
outputDir,
87-
nitroServerPath,
119+
configFile,
88120
})
89121
}
90122

packages/start-plugin-core/src/prerender.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ export async function prerender({
8585
cleanup = async () => {
8686
await previewServer.close()
8787
}
88+
89+
// Wait for the server to be ready (handles Nitro's child process startup time)
90+
await waitForServerReady(baseUrl, logger)
8891
}
8992

9093
const isRedirectResponse = (res: Response) => {
@@ -352,13 +355,48 @@ async function startPreviewServer(
352355
const vite = await import('vite')
353356

354357
try {
355-
return await vite.preview({
358+
const server = await vite.preview({
356359
configFile,
357360
preview: {
358361
port: 0,
359362
open: false,
360363
},
361364
})
365+
366+
// Check if Nitro's vite plugin is active (it spawns a child process)
367+
const hasNitroPlugin = server.config.plugins.some((p) => {
368+
if (typeof p !== 'object' || p === null) return false
369+
if (!('name' in p)) return false
370+
return typeof p.name === 'string' && p.name.startsWith('nitro:')
371+
})
372+
373+
if (hasNitroPlugin) {
374+
// Wrap the close method to handle Nitro's child process cleanup
375+
// Nitro's configurePreviewServer spawns a child process and registers
376+
// SIGINT/SIGHUP handlers to kill it. Since previewServer.close() doesn't
377+
// trigger these signals, we need to emit SIGHUP ourselves.
378+
const originalClose = server.close.bind(server)
379+
server.close = async () => {
380+
// Temporarily override process.exit to prevent Nitro's handler from
381+
// exiting our process when we emit SIGHUP
382+
const originalExit = process.exit
383+
process.exit = (() => {}) as typeof process.exit
384+
385+
// Emit SIGHUP to trigger Nitro's child process cleanup
386+
process.emit('SIGHUP', 'SIGHUP')
387+
388+
// Restore process.exit
389+
process.exit = originalExit
390+
391+
// Give the child process a moment to terminate
392+
await new Promise((resolve) => setTimeout(resolve, 100))
393+
394+
// Now close the preview server
395+
return originalClose()
396+
}
397+
}
398+
399+
return server
362400
} catch (error) {
363401
throw new Error(
364402
'Failed to start the Vite preview server for prerendering',
@@ -378,3 +416,30 @@ function getResolvedUrl(previewServer: PreviewServer): URL {
378416

379417
return new URL(baseUrl)
380418
}
419+
420+
async function waitForServerReady(
421+
baseUrl: URL,
422+
logger: ReturnType<typeof createLogger>,
423+
timeout = 30000,
424+
): Promise<void> {
425+
const startTime = Date.now()
426+
const checkInterval = 100
427+
428+
while (Date.now() - startTime < timeout) {
429+
try {
430+
const response = await fetch(new URL('/', baseUrl))
431+
// Server is ready if we get any response (even 404 is fine, we just need it to not error)
432+
if (response.status < 500) {
433+
logger.info('Server is ready')
434+
return
435+
}
436+
} catch {
437+
// Server not ready yet, retry
438+
}
439+
await new Promise((resolve) => setTimeout(resolve, checkInterval))
440+
}
441+
442+
throw new Error(
443+
`Server at ${baseUrl} did not become ready within ${timeout}ms`,
444+
)
445+
}

0 commit comments

Comments
 (0)