Skip to content

Commit

Permalink
feat(one): working Vercel builds using Build Output API v3
Browse files Browse the repository at this point in the history
  • Loading branch information
theonetheycallneo authored Feb 21, 2025
1 parent a0da7cc commit 134c7ed
Show file tree
Hide file tree
Showing 26 changed files with 2,736 additions and 1,946 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"patch-package": "^8.0.0",
"playwright": "^1.49.1",
"sst": "^3.6.18",
"ts-pattern": "^5.6.2",
"tsx": "^4.19.0",
"turbo": "^2.1.0",
"typescript": "^5.7.3",
Expand Down
27 changes: 15 additions & 12 deletions packages/one/src/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@ import {
build as vxrnBuild,
type ClientManifestEntry,
} from 'vxrn'

import * as constants from '../constants'
import { setServerGlobals } from '../server/setServerGlobals'
import { toAbsolute } from '../utils/toAbsolute'
import { getManifest } from '../vite/getManifest'
import { loadUserOneOptions } from '../vite/loadConfig'
import { runWithAsyncLocalContext } from '../vite/one-server-only'
import type { One, RouteInfo } from '../vite/types'
import { buildVercelOutputDirectory } from '../vercel/build/buildVercelOutputDirectory'

import { buildPage } from './buildPage'
import { checkNodeVersion } from './checkNodeVersion'
import { labelProcess } from './label-process'

const { ensureDir } = FSExtra
const { ensureDir, writeJSON } = FSExtra

process.on('uncaughtException', (err) => {
console.error(err?.message || err)
Expand Down Expand Up @@ -187,9 +190,10 @@ export async function build(args: {
return output as RollupOutput
}

let apiOutput: RollupOutput | null = null
if (manifest.apiRoutes.length) {
console.info(`\n 🔨 build api routes\n`)
await buildCustomRoutes('api', manifest.apiRoutes)
apiOutput = await buildCustomRoutes('api', manifest.apiRoutes)
}

const builtMiddlewares: Record<string, string> = {}
Expand Down Expand Up @@ -483,23 +487,22 @@ export async function build(args: {
constants: JSON.parse(JSON.stringify({ ...constants })) as any,
}

await FSExtra.writeJSON(toAbsolute(`dist/buildInfo.json`), buildInfoForWriting)
await writeJSON(toAbsolute(`dist/buildInfo.json`), buildInfoForWriting)

let postBuildLogs: string[] = []

const platform = oneOptions.web?.deploy ?? options.server?.platform
postBuildLogs.push(`[one.build] platform ${platform}`)

switch (platform) {
case 'vercel': {
await FSExtra.writeFile(
join(options.root, 'dist', 'index.js'),
`import { serve } from 'one/serve'
export const handler = await serve()
export const { GET, POST, PUT, PATCH, OPTIONS } = handler`
)

postBuildLogs.push(`wrote vercel entry to: ${join('.', 'dist', 'index.js')}`)
postBuildLogs.push(`point vercel outputDirectory to dist`)
await buildVercelOutputDirectory({
apiOutput,
buildInfoForWriting,
clientDir,
oneOptionsRoot: options.root,
postBuildLogs,
})

break
}
Expand Down
2 changes: 1 addition & 1 deletion packages/one/src/server/createRoutesManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getServerManifest } from './getServerManifest'

import type { One, RouteInfo } from '../vite/types'

export { type Options } from '../router/getRoutes'
export type { Options } from '../router/getRoutes'

export type RouteInfoCompiled = RouteInfo & {
compiledRegex: RegExp
Expand Down
126 changes: 126 additions & 0 deletions packages/one/src/vercel/build/buildVercelOutputDirectory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { join, resolve } from 'node:path'

import FSExtra from 'fs-extra'
import type { RollupOutput } from 'rollup'
import { isMatching, P } from 'ts-pattern'

import { createApiServerlessFunction } from './generate/createApiServerlessFunction'
import { createSsrServerlessFunction } from './generate/createSsrServerlessFunction'
import { serverlessVercelNodeJsConfig } from './config/vc-config-base'
import { serverlessVercelPackageJson } from './config/vc-package-base'
import { vercelBuildOutputConfig } from './config/vc-build-output-config-base'

import type { One } from '../../vite/types'

const { copy, ensureDir, 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)
}
}

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.page,
compiledRoute.code,
oneOptionsRoot,
postBuildLogs
)
} else {
console.warn('\n 🔨[one.build][vercel] apiRoute missing code compilation for', route.file)
}
}
}

const vercelOutputFunctionsDir = join(oneOptionsRoot, 'dist', `.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.page,
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 vercelMiddlewareDir = join(oneOptionsRoot, 'dist', '.vercel/output/functions/_middleware')
await ensureDir(vercelMiddlewareDir)
postBuildLogs.push(
`[one.build][vercel] copying middlewares from ${join(oneOptionsRoot, 'dist', 'middlewares')} to ${vercelMiddlewareDir}`
)
await moveAllFiles(resolve(join(oneOptionsRoot, 'dist', 'middlewares')), vercelMiddlewareDir)
const vercelMiddlewarePackageJsonFilePath = resolve(join(vercelMiddlewareDir, 'index.js'))
postBuildLogs.push(
`[one.build][vercel] writing package.json to ${vercelMiddlewarePackageJsonFilePath}`
)
await writeJSON(vercelMiddlewarePackageJsonFilePath, serverlessVercelPackageJson)
postBuildLogs.push(
`[one.build][vercel] writing .vc-config.json to ${join(vercelMiddlewareDir, '.vc-config.json')}`
)
await writeJSON(resolve(join(vercelMiddlewareDir, '.vc-config.json')), {
...serverlessVercelNodeJsConfig,
handler: '_middleware.js',
})

const vercelOutputStaticDir = resolve(join(oneOptionsRoot, 'dist', '.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
const vercelConfigFilePath = resolve(
join(oneOptionsRoot, 'dist', '.vercel/output', 'config.json')
)
await writeJSON(vercelConfigFilePath, vercelBuildOutputConfig)
postBuildLogs.push(`[one.build] wrote vercel config to: ${vercelConfigFilePath}`)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Documentation - Vercel Build Output v3 Config
// https://vercel.com/docs/build-output-api/v3/configuration#config.json-supported-properties
export const vercelBuildOutputConfig = {
version: 3,
// https://vercel.com/docs/build-output-api/v3/configuration#routes
routes: [
{
src: '/(.*)',
status: 200,
},
],
}
22 changes: 22 additions & 0 deletions packages/one/src/vercel/build/config/vc-config-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Documentation - Vercel Build Output v3 Node.js Config
// https://vercel.com/docs/build-output-api/v3/primitives#node.js-config
export const serverlessVercelNodeJsConfig = {
environment: {},
runtime: 'nodejs20.x',
handler: 'entrypoint/index.js',
launcherType: 'Nodejs',
shouldAddHelpers: true,
shouldAddSourceMapSupport: true,
// @TODO: We could support edge functions in the future.
// Requires a larger discusion of how to handle edge functions in general.
// +ssr-edge.tsx or +edge.tsx down the road.
// https://vercel.com/docs/build-output-api/v3/primitives#edge-functions
// runtime: 'edge',
// regions: 'all',
// @TODO: We could support ISR in the future as well.
// Requires a larger discusion of how to handle ISR in general.
// https://vercel.com/docs/build-output-api/v3/primitives#prerender-functions
// We would need to generate the bypassToken and copy *.html fallback files to the *.func folder.
// https://vercel.com/docs/build-output-api/v3/primitives#fallback-static-file
// bypassToken?: string;
}
1 change: 1 addition & 0 deletions packages/one/src/vercel/build/config/vc-package-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const serverlessVercelPackageJson = { type: 'module' }
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { join, resolve } from 'node:path'

import fs from 'fs-extra'

import { serverlessVercelPackageJson } from '../config/vc-package-base'
import { serverlessVercelNodeJsConfig } from '../config/vc-config-base'

// Documentation - Vercel Build Output v3
// https://vercel.com/docs/build-output-api/v3#build-output-api-v3
export async function createApiServerlessFunction(
pageName: string,
code: string,
oneOptionsRoot: string,
postBuildLogs: string[]
) {
try {
postBuildLogs.push(`[one.build][vercel.createSsrServerlessFunction] pageName: ${pageName}`)

const funcFolder = join(oneOptionsRoot, 'dist', `.vercel/output/functions/${pageName}.func`)
await fs.ensureDir(funcFolder)

if (code.includes('react')) {
postBuildLogs.push(
`[one.build][vercel.createSsrServerlessFunction] detected react in depenency tree for ${pageName}`
)
await fs.copy(
resolve(join(oneOptionsRoot, '..', '..', 'node_modules', 'react')),
resolve(join(funcFolder, 'node_modules', 'react'))
)
}

const distAssetsFolder = resolve(join(funcFolder, 'assets'))
postBuildLogs.push(
`[one.build][vercel.createSsrServerlessFunction] copy shared assets to ${distAssetsFolder}`
)
await fs.copy(resolve(join(oneOptionsRoot, 'dist', 'api', 'assets')), 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, 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 ${pageName}`,
e
)
}
}
Loading

0 comments on commit 134c7ed

Please sign in to comment.