UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

284 lines (259 loc) 10 kB
import { join, resolve } from 'node:path' import FSExtra from 'fs-extra' import type { RollupOutput } from 'rollup' import { isMatching, P } from 'ts-pattern' import type { One } from '../../vite/types' import { vercelBuildOutputConfigBase } from './config/vc-build-output-config-base' import { serverlessVercelNodeJsConfig } from './config/vc-config-base' import { serverlessVercelPackageJson } from './config/vc-package-base' import { createApiServerlessFunction } from './generate/createApiServerlessFunction' import { createSsrServerlessFunction } from './generate/createSsrServerlessFunction' import { getPathFromRoute } from './getPathFromRoute' const { copy, ensureDir, existsSync, writeJSON } = FSExtra async function moveAllFiles(src: string, dest: string) { try { await copy(src, dest, { overwrite: true, errorOnExist: false }) } catch (err) { console.error('Error moving files:', err) } } function getMiddlewaresByNamedRegex(buildInfoForWriting: One.BuildInfo) { return buildInfoForWriting.manifest.allRoutes .filter((r) => r.middlewares && r.middlewares.length > 0) .map((r) => [ r.namedRegex, r.middlewares!.map((m) => m.contextKey.startsWith('dist/middlewares/') ? m.contextKey.substring('dist/middlewares/'.length) : m.contextKey ), ]) .sort((a, b) => b[0].length - a[0].length) } export const buildVercelOutputDirectory = async ({ apiOutput, buildInfoForWriting, clientDir, oneOptionsRoot, postBuildLogs, }: { apiOutput: RollupOutput | null buildInfoForWriting: One.BuildInfo clientDir: string oneOptionsRoot: string postBuildLogs: string[] }) => { const { routeToBuildInfo } = buildInfoForWriting if (apiOutput) { const compiltedApiRoutes = (apiOutput?.output ?? []).filter((o) => isMatching({ code: P.string, facadeModuleId: P.string }, o) ) for (const route of buildInfoForWriting.manifest.apiRoutes) { const compiledRoute = compiltedApiRoutes.find((compiled) => { const flag = compiled.facadeModuleId.includes(route.file.replace('./', '')) return flag }) if (compiledRoute) { postBuildLogs.push( `[one.build][vercel] generating serverless function for apiRoute ${route.page}` ) await createApiServerlessFunction( route, compiledRoute.code, oneOptionsRoot, postBuildLogs ) } else { console.warn( '\n 🔨[one.build][vercel] apiRoute missing code compilation for', route.file ) } } } const vercelOutputFunctionsDir = join(oneOptionsRoot, '.vercel/output/functions') await ensureDir(vercelOutputFunctionsDir) for (const route of buildInfoForWriting.manifest.pageRoutes) { switch (route.type) { case 'ssr': { // Server Side Rendered const builtPageRoute = routeToBuildInfo[route.file] if (builtPageRoute) { postBuildLogs.push( `[one.build][vercel] generate serverless function for ${route.page} with ${route.type}` ) await createSsrServerlessFunction( route, buildInfoForWriting, oneOptionsRoot, postBuildLogs ) } break } default: // no-op, these will be copied from built dist/client into .vercel/output/static // postBuildLogs.push(`[one.build][vercel] pageRoute will be copied to .vercel/output/static for ${route.page} with ${route.type}`) break } } const distMiddlewareDir = resolve(join(oneOptionsRoot, 'dist', 'middlewares')) if (existsSync(distMiddlewareDir)) { const vercelMiddlewareDir = resolve( join(oneOptionsRoot, '.vercel/output/functions/_middleware.func') ) await ensureDir(vercelMiddlewareDir) postBuildLogs.push( `[one.build][vercel] copying middlewares from ${distMiddlewareDir} to ${vercelMiddlewareDir}` ) await moveAllFiles( resolve(join(oneOptionsRoot, 'dist', 'middlewares')), vercelMiddlewareDir ) const vercelMiddlewarePackageJsonFilePath = resolve( join(vercelMiddlewareDir, 'package.json') ) postBuildLogs.push( `[one.build][vercel] writing package.json to ${vercelMiddlewarePackageJsonFilePath}` ) await writeJSON(vercelMiddlewarePackageJsonFilePath, serverlessVercelPackageJson) const wrappedMiddlewareEntryPointFilename = '_wrapped_middleware.js' const wrappedMiddlewareEntryPointPath = resolve( join(vercelMiddlewareDir, wrappedMiddlewareEntryPointFilename) ) const middlewaresByNamedRegex = getMiddlewaresByNamedRegex(buildInfoForWriting) const middlewaresToVariableNameMap = middlewaresByNamedRegex.reduce( (acc, [namedRegex, middlewares]) => { ;(Array.isArray(middlewares) ? middlewares : [middlewares]).forEach( (middleware) => { const middlewareVariableName = middleware .replace(/\.[a-z]+$/, '') .replaceAll('/', '_') acc[middleware] = middlewareVariableName } ) return acc }, {} ) await FSExtra.writeFile( wrappedMiddlewareEntryPointPath, ` const middlewaresByNamedRegex = ${JSON.stringify(middlewaresByNamedRegex)} ${Object.entries(middlewaresToVariableNameMap) .map(([path, variableName]) => `import ${variableName} from './${path}'`) .join('\n')} function getMiddleware(path) { switch (path){ ${Object.entries(middlewaresToVariableNameMap) .map(([path, variableName]) => `case '${path}': return ${variableName}`) .join('\n')} default: return null } } const next = (e) => { const t = new Headers(null == e ? void 0 : e.headers) t.set('x-middleware-next', '1') return new Response(null, { ...e, headers: t }) } const wrappedMiddlewareFunction = (request, event) => { const url = new URL(request.url) const pathname = url.pathname // Find matching middlewares for this request const matchingMiddlewares = middlewaresByNamedRegex .filter(([namedRegex]) => new RegExp(namedRegex).test(pathname)) .reduce((prev, current) => prev.length > current[1]?.length ? prev : current[1], []); // Import and execute the middleware function const boundNext = () => { if (matchingMiddlewares.length === 0) { return next(request) } const middleware = getMiddleware(matchingMiddlewares.shift()) return middleware ? middleware({request, event, next: boundNext}) : next(request) }; return boundNext() } export { wrappedMiddlewareFunction as default } ` ) const middlewareVercelConfigFilePath = resolve( join(vercelMiddlewareDir, '.vc-config.json') ) postBuildLogs.push( `[one.build][vercel] writing .vc-config.json to ${middlewareVercelConfigFilePath}` ) await writeJSON(middlewareVercelConfigFilePath, { runtime: 'edge', // Seems that middlewares only work with edge runtime entrypoint: wrappedMiddlewareEntryPointFilename, }) } const vercelOutputStaticDir = resolve(join(oneOptionsRoot, '.vercel/output/static')) await ensureDir(vercelOutputStaticDir) postBuildLogs.push( `[one.build][vercel] copying static files from ${clientDir} to ${vercelOutputStaticDir}` ) await moveAllFiles(clientDir, vercelOutputStaticDir) // Documentation - Vercel Build Output v3 config.json // https://vercel.com/docs/build-output-api/v3/configuration#config.json-supported-properties // Generate loader routes for SSR pages // These intercept /assets/*_vxrn_loader.js requests and route to the SSR function const ssrLoaderRoutes = buildInfoForWriting.manifest.pageRoutes .filter((r) => r.type === 'ssr') .map((r) => { const pagePath = getPathFromRoute(r) || '/' // Convert page path to loader asset pattern // /ssr-page -> ssr-page // /dynamic/:id -> dynamic_:id (getPathFromRoute converts [id] to :id) const cleanPath = pagePath.slice(1).replace(/\//g, '_') // Build regex pattern for the loader asset // Replace :param with capture groups for dynamic segments // The loader URL pattern uses the actual param value, not :param // e.g., /dynamic/123 -> /assets/dynamic_123_12345_vxrn_loader.js const loaderPattern = cleanPath.replace(/:([^_]+)/g, '(?<$1>[^_]+)') // Match the loader file pattern: {path}_{cacheKey}_vxrn_loader.js // Also handle _refetch_ pattern for cache busting const src = `^/assets/${loaderPattern}(?:_refetch_\\d+)?_\\d+_vxrn_loader\\.js$` // Build destination with captured params let dest = `${pagePath}?__loader=1` const paramMatches = cleanPath.match(/:([^_]+)/g) if (paramMatches) { for (const match of paramMatches) { const paramName = match.slice(1) // remove leading : dest += `&${paramName}=$${paramName}` } } return { src, dest } }) const vercelConfigFilePath = resolve( join(oneOptionsRoot, '.vercel/output', 'config.json') ) await writeJSON(vercelConfigFilePath, { ...vercelBuildOutputConfigBase, routes: [ ...vercelBuildOutputConfigBase.routes, ...(existsSync(distMiddlewareDir) ? [ { src: '/(.*)', middlewarePath: '_middleware', continue: true, }, ] : []), { handle: 'rewrite', }, // SSR loader routes must come before dynamic page routes ...ssrLoaderRoutes, ...buildInfoForWriting.manifest.allRoutes .filter((r) => r.routeKeys && Object.keys(r.routeKeys).length > 0) .map((r) => ({ src: r.namedRegex, dest: `${getPathFromRoute(r) || '/'}?${Object.entries(r.routeKeys) .map(([k, v]) => `${k}=$${v}`) .join('&')}`, })), ], }) postBuildLogs.push(`[one.build] wrote vercel config to: ${vercelConfigFilePath}`) }