Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/start-plugin-core/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,12 +347,17 @@ export function TanStackStartVitePluginCore(
// Build the SSR bundle
await builder.build(server)
}
const serverBundle = getBundle(VITE_ENVIRONMENT_NAMES.server)
await postServerBuild({ builder, startConfig, serverBundle })
},
},
}
},
buildApp: {
order: 'post',
async handler(builder) {
const { startConfig } = getConfig()
await postServerBuild({ builder, startConfig })
},
},
Comment on lines +354 to +360

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have moved the postServerBuild step to the buildApp hook to ensure all plugins finish their builds before we start the preview server.

},
tanStackStartRouter(startPluginOpts, getConfig, corePluginOpts),
// N.B. TanStackStartCompilerPlugin must be before the TanStackServerFnPlugin
Expand Down
5 changes: 1 addition & 4 deletions packages/start-plugin-core/src/post-server-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import { buildSitemap } from './build-sitemap'
import { VITE_ENVIRONMENT_NAMES } from './constants'
import { prerender } from './prerender'
import type { TanStackStartOutputConfig } from './schema'
import type { Rollup, ViteBuilder } from 'vite'
import type { ViteBuilder } from 'vite'

export async function postServerBuild({
builder,
startConfig,
serverBundle,
}: {
builder: ViteBuilder
startConfig: TanStackStartOutputConfig
serverBundle: Rollup.OutputBundle
}) {
// If the user has not set a prerender option, we need to set it to true
// if the pages array is not empty and has sub options requiring for prerendering
Expand Down Expand Up @@ -57,7 +55,6 @@ export async function postServerBuild({
await prerender({
startConfig,
builder,
serverBundle,
})
}

Expand Down
97 changes: 35 additions & 62 deletions packages/start-plugin-core/src/prerender.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { existsSync, promises as fsp, rmSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import { promises as fsp } from 'node:fs'
import os from 'node:os'
import path from 'pathe'
import { joinURL, withBase, withoutBase } from 'ufo'
import { VITE_ENVIRONMENT_NAMES } from './constants'
import { createLogger } from './utils'
import { Queue } from './queue'
import type { Rollup, ViteBuilder } from 'vite'
import type { PreviewServer, ResolvedConfig, ViteBuilder } from 'vite'
import type { Page, TanStackStartOutputConfig } from './schema'

export async function prerender({
startConfig,
builder,
serverBundle,
}: {
startConfig: TanStackStartOutputConfig
builder: ViteBuilder
serverBundle: Rollup.OutputBundle
}) {
const logger = createLogger('prerender')
logger.info('Prerendering pages...')
Expand Down Expand Up @@ -60,27 +57,11 @@ export async function prerender({

const outputDir = clientEnv.config.build.outDir

const entryFile = findEntryFileInBundle(serverBundle)
let fullEntryFilePath = path.join(serverEnv.config.build.outDir, entryFile)
process.env.TSS_PRERENDERING = 'true'

if (!existsSync(fullEntryFilePath)) {
// if the file does not exist, we need to write the bundle to a temporary directory
// this can happen e.g. with nitro that postprocesses the bundle and thus does not write SSR build to disk
const bundleOutputDir = path.resolve(
serverEnv.config.root,
'.tanstack',
'start',
'prerender',
)
rmSync(bundleOutputDir, { recursive: true, force: true })
await writeBundleToDisk({ bundle: serverBundle, outDir: bundleOutputDir })
fullEntryFilePath = path.join(bundleOutputDir, entryFile)
}

const { default: serverEntrypoint } = await import(
pathToFileURL(fullEntryFilePath).toString()
)
// Start Vite preview server instead of importing module
const previewServer = await startPreviewServer(serverEnv.config)
const baseUrl = getResolvedUrl(previewServer)

const isRedirectResponse = (res: Response) => {
return res.status >= 300 && res.status < 400 && res.headers.get('location')
Expand All @@ -90,8 +71,9 @@ export async function prerender({
options?: RequestInit,
maxRedirects: number = 5,
): Promise<Response> {
const url = new URL(`http://localhost${path}`)
const response = await serverEntrypoint.fetch(new Request(url, options))
const url = new URL(path, baseUrl)
const request = new Request(url, options)
const response = await fetch(request)

if (isRedirectResponse(response) && maxRedirects > 0) {
const location = response.headers.get('location')!
Expand All @@ -116,6 +98,8 @@ export async function prerender({
})
} catch (error) {
logger.error(error)
} finally {
await previewServer.close()
}

function extractLinks(html: string): Array<string> {
Expand Down Expand Up @@ -278,46 +262,35 @@ export async function prerender({
}
}

function findEntryFileInBundle(bundle: Rollup.OutputBundle): string {
let entryFile: string | undefined
async function startPreviewServer(
viteConfig: ResolvedConfig,
): Promise<PreviewServer> {
const vite = await import('vite')

for (const [_name, file] of Object.entries(bundle)) {
if (file.type === 'chunk') {
if (file.isEntry) {
if (entryFile !== undefined) {
throw new Error(
`Multiple entry points found. Only one entry point is allowed.`,
)
}
entryFile = file.fileName
}
}
}
if (entryFile === undefined) {
throw new Error(`No entry point found in the bundle.`)
try {
return await vite.preview({
configFile: viteConfig.configFile,
preview: {
port: 0,
open: false,
},
})
} catch (error) {
throw new Error(
'Failed to start the Vite preview server for prerendering',
{
cause: error,
},
)
}
return entryFile
}

export async function writeBundleToDisk({
bundle,
outDir,
}: {
bundle: Rollup.OutputBundle
outDir: string
}) {
const createdDirs = new Set<string>()
function getResolvedUrl(previewServer: PreviewServer): URL {
const baseUrl = previewServer.resolvedUrls?.local[0]

for (const [fileName, asset] of Object.entries(bundle)) {
const fullPath = path.join(outDir, fileName)
const dir = path.dirname(fullPath)
const content = asset.type === 'asset' ? asset.source : asset.code

if (!createdDirs.has(dir)) {
await fsp.mkdir(dir, { recursive: true })
createdDirs.add(dir)
}

await fsp.writeFile(fullPath, content)
if (!baseUrl) {
throw new Error('No resolved URL is available from the Vite preview server')
}

return new URL(baseUrl)
}