UNPKG

one

Version:

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

153 lines (130 loc) 4.87 kB
import { dirname, join, resolve } from 'node:path' import { parseSync } from 'oxc-parser' import MagicString from 'magic-string' import { resolvePath } from '@vxrn/resolve' import FSExtra from 'fs-extra' import fs from 'fs-extra' import type { RouteInfo } from '../../../vite/types' import { serverlessVercelNodeJsConfig } from '../config/vc-config-base' import { serverlessVercelPackageJson } from '../config/vc-package-base' import { getPathFromRoute } from '../getPathFromRoute' // Documentation - Vercel Build Output v3 // https://vercel.com/docs/build-output-api/v3#build-output-api-v3 export async function createApiServerlessFunction( route: RouteInfo<string>, code: string, oneOptionsRoot: string, postBuildLogs: string[], outDir = 'dist' ) { try { const path = getPathFromRoute(route, { includeIndex: true }) postBuildLogs.push( `[one.build][vercel.createSsrServerlessFunction] pageName: ${path}` ) const funcFolder = join(oneOptionsRoot, `.vercel/output/functions/${path}.func`) await fs.ensureDir(funcFolder) if (code.includes('react')) { postBuildLogs.push( `[one.build][vercel.createSsrServerlessFunction] detected react in depenency tree for ${path}` ) const reactPath = dirname(resolvePath('react/package.json', oneOptionsRoot)) await fs.copy( resolve(reactPath), resolve(join(funcFolder, 'node_modules', 'react')) ) } const distAssetsFolder = resolve(join(funcFolder, 'assets')) postBuildLogs.push( `[one.build][vercel.createSsrServerlessFunction] copy shared assets to ${distAssetsFolder}` ) const sourceAssetsFolder = resolve(join(oneOptionsRoot, outDir, 'api', 'assets')) if (await FSExtra.pathExists(sourceAssetsFolder)) { await fs.copy(sourceAssetsFolder, distAssetsFolder) } await fs.ensureDir(resolve(join(funcFolder, 'entrypoint'))) const entrypointFilePath = resolve(join(funcFolder, 'entrypoint', 'index.js')) postBuildLogs.push( `[one.build][vercel.createSsrServerlessFunction] writing entrypoint to ${entrypointFilePath}` ) await fs.writeFile(entrypointFilePath, wrapHandlerFunctions(code)) const packageJsonFilePath = resolve(join(funcFolder, 'package.json')) postBuildLogs.push( `[one.build][vercel.createSsrServerlessFunction] writing package.json to ${packageJsonFilePath}` ) await fs.writeJSON(packageJsonFilePath, serverlessVercelPackageJson) postBuildLogs.push( `[one.build][vercel.createSsrServerlessFunction] writing .vc-config.json to ${join(funcFolder, '.vc-config.json')}` ) // Documentation - Vercel Build Output v3 Node.js Config // https://vercel.com/docs/build-output-api/v3/primitives#node.js-config return fs.writeJson(join(funcFolder, '.vc-config.json'), { ...serverlessVercelNodeJsConfig, handler: 'entrypoint/index.js', }) } catch (e) { console.error( `[one.build][vercel.createSsrServerlessFunction] failed to generate func for ${route.file}`, e ) } } /** * Vercel won't pass `{ params }` as the second argument to the handler function. * So we need to wrap the handler function to parse the params from the request, * and pass them to the handler function. */ function wrapHandlerFunctions(code: string): string { let parsed: any try { parsed = parseSync('api.js', code) } catch { return code } if (!parsed?.program) return code const functionNamesToHandle = new Set([ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', ]) const ms = new MagicString(code) const wrappersToAppend: string[] = [] for (const item of parsed.program.body || []) { let fnDecl: any = null let isExported = false let exportNode: any = null if ( item.type === 'ExportNamedDeclaration' && item.declaration?.type === 'FunctionDeclaration' ) { fnDecl = item.declaration isExported = true exportNode = item } else if (item.type === 'FunctionDeclaration') { fnDecl = item isExported = false } if (!fnDecl || !fnDecl.id?.name || !functionNamesToHandle.has(fnDecl.id.name)) { continue } const name = fnDecl.id.name const originalName = `orig_${name}` if (isExported) { ms.remove(exportNode.start, fnDecl.start) } ms.overwrite(fnDecl.id.start, fnDecl.id.end, originalName) const exportPrefix = isExported ? 'export ' : '' wrappersToAppend.push( `\n${exportPrefix}function ${name}(request) {\n const url = new URL(request.url);\n const params = Object.fromEntries(url.searchParams.entries());\n return ${originalName}(request, { params });\n}` ) } if (wrappersToAppend.length > 0) { ms.append(wrappersToAppend.join('\n')) return ms.toString() } return code }