one
Version:
One is a new React Framework that makes Vite serve both native and web.
550 lines (489 loc) ⢠18 kB
text/typescript
import { join } from 'node:path'
import FSExtra from 'fs-extra'
import * as constants from '../constants'
import type { LoaderProps, RenderApp } from '../types'
import { getLoaderPath, getPreloadCSSPath, getPreloadPath } from '../utils/cleanUrl'
import { isResponse } from '../utils/isResponse'
import { toAbsolute } from '../utils/toAbsolute'
import { replaceLoader } from '../vite/replaceLoader'
import type { One, RouteInfo } from '../vite/types'
const { readFile, outputFile } = FSExtra
// Convert URL path (with forward slashes) to filesystem path for cross-platform compatibility
function urlPathToFilePath(urlPath: string): string {
// Remove leading slash and split by forward slash (URL separator)
const parts = urlPath.replace(/^\//, '').split('/')
return join(...parts)
}
// timing helper for build profiling
const buildTiming = process.env.ONE_BUILD_TIMING === '1'
const timings: Record<string, number[]> = {}
function recordTiming(label: string, ms: number) {
if (!buildTiming) return
;(timings[label] ||= []).push(ms)
}
export function printBuildTimings() {
if (!buildTiming) return
console.info('\nš Build timing breakdown:')
for (const [label, times] of Object.entries(timings)) {
const avg = times.reduce((a, b) => a + b, 0) / times.length
const total = times.reduce((a, b) => a + b, 0)
console.info(
` ${label}: ${avg.toFixed(1)}ms avg, ${total.toFixed(0)}ms total (${times.length} calls)`
)
}
}
export async function buildPage(
serverEntry: string,
path: string,
relativeId: string,
params: any,
foundRoute: RouteInfo<string>,
clientManifestEntry: any,
staticDir: string,
clientDir: string,
builtMiddlewares: Record<string, string>,
serverJsPath: string,
preloads: string[],
allCSS: string[],
routePreloads: Record<string, string>,
allCSSContents?: string[],
criticalPreloads?: string[],
deferredPreloads?: string[],
useAfterLCP?: boolean,
useAfterLCPAggressive?: boolean
): Promise<One.RouteBuildInfo> {
let t0 = performance.now()
const render = await getRender(serverEntry)
recordTiming('getRender', performance.now() - t0)
const htmlPath = `${path.endsWith('/') ? `${removeTrailingSlash(path)}/index` : path}.html`
const clientJsPath = clientManifestEntry
? join(`dist/client`, clientManifestEntry.file)
: ''
const htmlOutPath = toAbsolute(join(staticDir, htmlPath))
const preloadPath = getPreloadPath(path)
const cssPreloadPath = getPreloadCSSPath(path)
let loaderPath = ''
let loaderData = {}
try {
// generate preload file with route module registration
const routeImports: string[] = []
const routeRegistrations: string[] = []
let routeIndex = 0
for (const [routeKey, bundlePath] of Object.entries(routePreloads)) {
const varName = `_r${routeIndex++}`
routeImports.push(`import * as ${varName} from "${bundlePath}"`)
routeRegistrations.push(`registerPreloadedRoute("${routeKey}", ${varName})`)
}
// Use window global for registration since ES module exports get tree-shaken
const registrationCalls = routeRegistrations.map((call) =>
call.replace('registerPreloadedRoute(', 'window.__oneRegisterPreloadedRoute(')
)
const preloadContent = [
// import all route modules
...routeImports,
// static imports for cache warming (original behavior)
...preloads.map((preload) => `import "${preload}"`),
// register all route modules using window global
...registrationCalls,
].join('\n')
t0 = performance.now()
await FSExtra.writeFile(
join(clientDir, urlPathToFilePath(preloadPath)),
preloadContent
)
recordTiming('writePreload', performance.now() - t0)
// Generate CSS preload file with prefetch (on hover) and inject (on navigation) functions
// Deduplicate CSS URLs to avoid loading the same file multiple times
const uniqueCSS = [...new Set(allCSS)]
const cssPreloadContent = `
const CSS_TIMEOUT = 1000
const cssUrls = ${JSON.stringify(uniqueCSS)}
// Global cache for loaded CSS - avoids DOM queries and tracks across navigations
const loaded = (window.__oneLoadedCSS ||= new Set())
// Prefetch CSS without applying - called on link hover
export function prefetchCSS() {
cssUrls.forEach(href => {
if (loaded.has(href)) return
if (document.querySelector(\`link[href="\${href}"]\`)) return
const link = document.createElement('link')
link.rel = 'prefetch'
link.as = 'style'
link.href = href
document.head.appendChild(link)
})
}
// Inject CSS to apply styles - called on actual navigation
export function injectCSS() {
return Promise.all(cssUrls.map(href => {
// Skip if already loaded
if (loaded.has(href)) return Promise.resolve()
// Remove any prefetch link for this href
const prefetchLink = document.querySelector(\`link[rel="prefetch"][href="\${href}"]\`)
if (prefetchLink) prefetchLink.remove()
// Skip if stylesheet already exists in DOM
if (document.querySelector(\`link[rel="stylesheet"][href="\${href}"]\`)) {
loaded.add(href)
return Promise.resolve()
}
return new Promise(resolve => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
const timeoutId = setTimeout(() => {
console.warn('[one] CSS load timeout:', href)
loaded.add(href)
resolve()
}, CSS_TIMEOUT)
link.onload = link.onerror = () => {
clearTimeout(timeoutId)
loaded.add(href)
resolve()
}
document.head.appendChild(link)
})
}))
}
// For backwards compatibility, also prefetch on import
prefetchCSS()
`
t0 = performance.now()
await FSExtra.writeFile(
join(clientDir, urlPathToFilePath(cssPreloadPath)),
cssPreloadContent
)
recordTiming('writeCSSPreload', performance.now() - t0)
t0 = performance.now()
const exported = await import(toAbsolute(serverJsPath))
recordTiming('importServerModule', performance.now() - t0)
const loaderProps: LoaderProps = { path, params }
// Build matches array for useMatches() hook
const matches: One.RouteMatch[] = []
// Run layout loaders in parallel
t0 = performance.now()
if (foundRoute.layouts?.length) {
const layoutResults = await Promise.all(
foundRoute.layouts.map(async (layout) => {
try {
const layoutServerPath = layout.loaderServerPath
if (!layoutServerPath) {
return { contextKey: layout.contextKey, loaderData: undefined }
}
const layoutExported = await import(
toAbsolute(join('./', 'dist/server', layoutServerPath))
)
const layoutLoaderData = await layoutExported?.loader?.(loaderProps)
return { contextKey: layout.contextKey, loaderData: layoutLoaderData }
} catch (err) {
if (isResponse(err)) {
throw err
}
console.warn(
`[one] Warning: layout loader failed for ${layout.contextKey}:`,
err
)
return { contextKey: layout.contextKey, loaderData: undefined }
}
})
)
for (const result of layoutResults) {
matches.push({
routeId: result.contextKey,
pathname: path,
params: params || {},
loaderData: result.loaderData,
})
}
}
recordTiming('layoutLoaders', performance.now() - t0)
// Run page loader
t0 = performance.now()
let loaderRedirectInfo: { path: string; status: number } | null = null
if (exported.loader) {
try {
loaderData = (await exported.loader?.(loaderProps)) ?? null
} catch (err) {
// handle thrown responses (e.g., throw redirect('/login'))
// extract redirect info so we can generate a static redirect loader file
if (isResponse(err)) {
loaderRedirectInfo = extractRedirectInfo(err as Response)
} else {
throw err
}
}
// handle returned redirect responses (e.g., return redirect('/login'))
// check both isResponse and constructor name for cross-context compatibility
if (
!loaderRedirectInfo &&
loaderData &&
(isResponse(loaderData) ||
loaderData instanceof Response ||
loaderData?.constructor?.name === 'Response')
) {
loaderRedirectInfo = extractRedirectInfo(loaderData as Response)
loaderData = {}
}
if (clientJsPath) {
const loaderPartialPath = join(clientDir, urlPathToFilePath(getLoaderPath(path)))
if (loaderRedirectInfo) {
// generate a static redirect loader ā the client detects __oneRedirect
// and navigates before the protected page ever renders
const redirectData = JSON.stringify({
__oneRedirect: loaderRedirectInfo.path,
__oneRedirectStatus: loaderRedirectInfo.status,
})
await outputFile(
loaderPartialPath,
`export function loader(){return ${redirectData}}`
)
loaderPath = getLoaderPath(path)
loaderData = {}
} else {
const code = await readFile(clientJsPath, 'utf-8')
const withLoader =
// super dirty to quickly make ssr loaders work until we have better
`
if (typeof document === 'undefined') globalThis.document = {}
` +
replaceLoader({
code,
loaderData,
})
await outputFile(loaderPartialPath, withLoader)
loaderPath = getLoaderPath(path)
}
}
}
recordTiming('pageLoader', performance.now() - t0)
// Add page match
matches.push({
routeId: foundRoute.file,
pathname: path,
params: params || {},
loaderData,
})
// ssr, we basically skip at build-time and just compile it the js we need
if (foundRoute.type !== 'ssr') {
// importing resetState causes issues :/
globalThis['__vxrnresetState']?.()
if (foundRoute.type === 'ssg') {
// Aggressive mode: only modulepreload critical scripts, skip deferred to prevent network saturation
// Regular after-lcp mode: modulepreload all scripts for parallel downloads, defer execution
// Default: all scripts load normally
const renderPreloads = criticalPreloads || preloads
const renderDeferredPreloads = useAfterLCPAggressive ? [] : deferredPreloads
t0 = performance.now()
let html = await render({
path,
preloads: renderPreloads,
deferredPreloads: renderDeferredPreloads,
loaderProps,
loaderData,
css: allCSS,
cssContents: allCSSContents,
mode: 'ssg',
routePreloads,
matches,
})
recordTiming('ssrRender', performance.now() - t0)
// Apply after-LCP script loading if enabled
// Load all preloads (not just critical) to ensure good TTI after first paint
if (useAfterLCP) {
html = applyAfterLCPScriptLoad(html, preloads)
}
t0 = performance.now()
await outputFile(htmlOutPath, html)
recordTiming('writeHTML', performance.now() - t0)
} else if (foundRoute.type === 'spa') {
// spa-shell: render if any parent layout has ssg/ssr render mode
const needsSpaShell = foundRoute.layouts?.some(
(layout) =>
layout.layoutRenderMode === 'ssg' || layout.layoutRenderMode === 'ssr'
)
if (needsSpaShell) {
// render root layout shell for SPA pages
globalThis['__vxrnresetState']?.()
const renderPreloads = criticalPreloads || preloads
const renderDeferredPreloads = deferredPreloads || []
// for spa-shell, include layout matches (not page match)
// matches array at this point has: [layout1, layout2, ..., page]
// we want just the layouts for spa-shell
const layoutMatches = matches.slice(0, -1)
t0 = performance.now()
let html = await render({
path,
preloads: renderPreloads,
deferredPreloads: renderDeferredPreloads,
loaderProps,
// don't pass loaderData for spa-shell - the page loader runs on client
// passing {} here would make useLoaderState think data is preloaded
loaderData: undefined,
css: allCSS,
cssContents: allCSSContents,
mode: 'spa-shell',
routePreloads,
matches: layoutMatches,
})
recordTiming('spaShellRender', performance.now() - t0)
if (useAfterLCP) {
html = applyAfterLCPScriptLoad(html, preloads)
}
t0 = performance.now()
await outputFile(htmlOutPath, html)
recordTiming('writeHTML', performance.now() - t0)
} else {
// Generate CSS - either inline styles or link tags
const cssOutput = allCSSContents
? allCSSContents
.filter(Boolean)
.map((content) => ` <style>${content}</style>`)
.join('\n')
: allCSS
.map((file) => ` <link rel="stylesheet" href=${file} />`)
.join('\n')
// Use separated preloads if available
const criticalScripts = (criticalPreloads || preloads)
.map((preload) => ` <script type="module" src="${preload}"></script>`)
.join('\n')
// Non-critical scripts as modulepreload hints only
const deferredLinks = (deferredPreloads || [])
.map(
(preload) =>
` <link rel="modulepreload" fetchPriority="low" href="${preload}"/>`
)
.join('\n')
await outputFile(
htmlOutPath,
`<!DOCTYPE html><html><head>
${constants.getSpaHeaderElements({ serverContext: { loaderProps, loaderData } })}
${criticalScripts}
${deferredLinks}
${cssOutput}
</head><body></body></html>`
)
}
}
}
} catch (err) {
const errMsg = err instanceof Error ? `${err.message}\n${err.stack}` : `${err}`
console.error(
`Error building static page at ${path} with id ${relativeId}:
${errMsg}
loaderData:\n\n${JSON.stringify(loaderData || null, null, 2)}
params:\n\n${JSON.stringify(params || null, null, 2)}`
)
console.error(err)
process.exit(1)
}
const middlewares = (foundRoute.middlewares || []).map(
(x) => builtMiddlewares[x.contextKey]
)
const cleanPath = path === '/' ? path : removeTrailingSlash(path)
return {
type: foundRoute.type,
css: allCSS,
cssContents: allCSSContents,
routeFile: foundRoute.file,
middlewares,
cleanPath,
preloadPath,
cssPreloadPath,
loaderPath,
clientJsPath,
serverJsPath,
htmlPath,
loaderData,
params,
path,
preloads,
criticalPreloads,
deferredPreloads,
}
}
async function getRender(serverEntry: string) {
let render: RenderApp | null = null
try {
const serverImport = await import(serverEntry)
render =
serverImport.default.render ||
// for an unknown reason this is necessary
serverImport.default.default?.render
if (typeof render !== 'function') {
console.error(`ā Error: didn't find render function in entry`, serverImport)
process.exit(1)
}
} catch (err) {
console.error(`ā Error importing the root entry:`)
console.error(` This error happened in the built file: ${serverEntry}`)
// @ts-expect-error
console.error(err['stack'])
process.exit(1)
}
return render
}
function removeTrailingSlash(path: string) {
return path.endsWith('/') ? path.slice(0, path.length - 1) : path
}
// extract redirect target from a Response object (e.g., from redirect())
function extractRedirectInfo(
response: Response
): { path: string; status: number } | null {
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location')
if (location) {
try {
const url = new URL(location)
return {
path: url.pathname + url.search + url.hash,
status: response.status,
}
} catch {
// relative URL
return { path: location, status: response.status }
}
}
}
return null
}
/**
* Transforms HTML to delay script execution until after first paint.
* Keeps modulepreload links so critical scripts download in parallel.
* Removes async script tags and adds a loader that executes scripts after paint.
*/
function applyAfterLCPScriptLoad(html: string, preloads: string[]): string {
// Remove all <script type="module" ... async> tags (prevents immediate execution)
// Keep modulepreload links so critical scripts download in parallel
html = html.replace(/<script\s+type="module"[^>]*async[^>]*><\/script>/gi, '')
// Create the loader script
// Nested setTimeout yields to event loop multiple times, letting browser settle before loading scripts
const loaderScript = `
<script>
(function() {
var scripts = ${JSON.stringify(preloads)};
function loadScripts() {
scripts.forEach(function(src) {
var script = document.createElement('script');
script.type = 'module';
script.src = src;
document.head.appendChild(script);
});
}
function waitIdle(n) {
if (n <= 0) {
requestAnimationFrame(function() {
requestAnimationFrame(loadScripts);
});
return;
}
setTimeout(function() {
setTimeout(function() {
waitIdle(n - 1);
}, 0);
}, 0);
}
waitIdle(5);
})();
</script>`
// Insert the loader script before </head>
html = html.replace('</head>', `${loaderScript}</head>`)
return html
}