one
Version:
One is a new React Framework that makes Vite serve both native and web.
191 lines (165 loc) • 7.13 kB
text/typescript
import { dirname, join, resolve } from 'node:path'
import generator from '@babel/generator'
import parser from '@babel/parser'
import traverse from '@babel/traverse'
import t from '@babel/types'
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[]
) {
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, 'dist', '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) {
const ast = parser.parse(code, {
sourceType: 'module',
})
// TODO: idk why the TypeScript type does not match the actual type. Seems that we should use `traverse.default`, but TypeScript thinks we should use `traverse` directly.
;((traverse as any).default as typeof traverse)(ast, {
FunctionDeclaration(path) {
const { node } = path
const functionNamesToHandle = [
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'HEAD',
'OPTIONS',
// TODO: more possibilities?
]
if (!node.id || !functionNamesToHandle.includes(node.id.name)) return
// TODO: may need to also check if the function is export in any way, if
// the isn't exported at all, we can skip.
if (node.extra && node.extra.isWrapper) return
if (node.extra && node.extra.isWrapped) return
const originalName = `orig_${node.id.name}`
const originalFunction = t.functionDeclaration(
t.identifier(originalName),
node.params,
node.body,
node.generator,
node.async
)
/* The first argument of the handler function, which is the request object. */
const requestIdentifier = t.identifier('request')
const wrapperParams = [requestIdentifier]
/* A local variable in the wrapper function to hold the URL object. */
const urlIdentifier = t.identifier('url')
/* A local variable in the wrapper function to hold parsed params. */
const paramsIdentifier = t.identifier('params')
const urlDecl = t.variableDeclaration('const', [
t.variableDeclarator(
urlIdentifier,
t.newExpression(t.identifier('URL') /* Node.js global */, [
t.memberExpression(requestIdentifier, t.identifier('url')) /* request.url */,
])
),
])
const paramsDecl = t.variableDeclaration('const', [
t.variableDeclarator(
paramsIdentifier,
t.callExpression(
t.memberExpression(t.identifier('Object'), t.identifier('fromEntries')),
[
t.callExpression(
t.memberExpression(
t.memberExpression(
urlIdentifier,
t.identifier('searchParams')
) /* url.searchParams */,
t.identifier('entries')
),
[]
),
]
)
),
])
const callOrigFnStatement = t.callExpression(t.identifier(originalName), [
requestIdentifier,
t.objectExpression([t.objectProperty(t.identifier('params'), paramsIdentifier)]),
])
const wrapperFunction = t.functionDeclaration(
t.identifier(node.id.name + ''),
wrapperParams,
t.blockStatement([urlDecl, paramsDecl, t.returnStatement(callOrigFnStatement)])
// No need to care if the wrapper function should be async,
// since we didn't use any await in the wrapper function, and we'll
// just return what the original function returns.
)
node.extra = node.extra || {}
node.extra.isWrapped = true
wrapperFunction.extra = wrapperFunction.extra || {}
wrapperFunction.extra.isWrapper = true
if (path.parentPath.isExportNamedDeclaration()) {
path.replaceWithMultiple([originalFunction, t.exportNamedDeclaration(wrapperFunction, [])])
} else {
path.replaceWithMultiple([originalFunction, wrapperFunction])
}
},
})
// TODO: idk why the TypeScript type does not match the actual type. Seems that we should use `generator.default`, but TypeScript thinks we should use `generator` directly.
const output = ((generator as any).default as typeof generator)(ast, {}).code
return output
}