Skip to content

Commit d9f273b

Browse files
rsbhclaude
andcommitted
feat: add Vercel deploy adapter via --adapter vercel flag
Adds post-build step that generates .vercel/output/ with: - Static assets from client build + content dir - Bundled serverless function (catch-all) - Build Output API config (v3) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ad3bae7 commit d9f273b

2 files changed

Lines changed: 144 additions & 2 deletions

File tree

packages/chronicle/src/cli/commands/build.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const buildCommand = new Command('build')
88
.description('Build for production')
99
.option('-c, --content <path>', 'Content directory')
1010
.option('-o, --outDir <path>', 'Output directory', 'dist')
11+
.option('--adapter <adapter>', 'Deploy adapter (vercel)')
1112
.action(async (options) => {
1213
const contentDir = resolveContentDir(options.content)
1314
const outDir = path.resolve(options.outDir)
@@ -35,7 +36,11 @@ export const buildCommand = new Command('build')
3536
},
3637
})
3738

38-
// Build server bundle (noExternal: true to bundle all deps for portability)
39+
// Build server bundle
40+
const serverEntry = options.adapter === 'vercel'
41+
? path.resolve(PACKAGE_ROOT, 'src/server/entry-vercel.ts')
42+
: path.resolve(PACKAGE_ROOT, 'src/server/entry-prod.ts')
43+
3944
console.log(chalk.gray('Building server...'))
4045
await build({
4146
...baseConfig,
@@ -44,9 +49,19 @@ export const buildCommand = new Command('build')
4449
},
4550
build: {
4651
outDir: path.join(outDir, 'server'),
47-
ssr: path.resolve(PACKAGE_ROOT, 'src/server/entry-prod.ts'),
52+
ssr: serverEntry,
4853
},
4954
})
5055

5156
console.log(chalk.green('Build complete →'), outDir)
57+
58+
// Run Vercel adapter post-build
59+
if (options.adapter === 'vercel') {
60+
const { buildVercelOutput } = await import('@/server/adapters/vercel')
61+
await buildVercelOutput({
62+
distDir: outDir,
63+
contentDir,
64+
projectRoot: process.cwd(),
65+
})
66+
}
5267
})
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import path from 'path'
2+
import fs from 'fs/promises'
3+
import { existsSync } from 'fs'
4+
import chalk from 'chalk'
5+
6+
interface VercelAdapterOptions {
7+
distDir: string
8+
contentDir: string
9+
projectRoot: string
10+
}
11+
12+
const CONTENT_EXTENSIONS = new Set([
13+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
14+
'.pdf', '.json', '.yaml', '.yml', '.txt',
15+
])
16+
17+
export async function buildVercelOutput(options: VercelAdapterOptions) {
18+
const { distDir, contentDir, projectRoot } = options
19+
const outputDir = path.resolve(projectRoot, '.vercel/output')
20+
21+
console.log(chalk.gray('Generating Vercel output...'))
22+
23+
// Clean previous output
24+
await fs.rm(outputDir, { recursive: true, force: true })
25+
26+
// Create output directories
27+
const staticDir = path.resolve(outputDir, 'static')
28+
const funcDir = path.resolve(outputDir, 'functions/index.func')
29+
await fs.mkdir(staticDir, { recursive: true })
30+
await fs.mkdir(funcDir, { recursive: true })
31+
32+
// 1. Copy client assets → .vercel/output/static/
33+
const clientDir = path.resolve(distDir, 'client')
34+
await copyDir(clientDir, staticDir)
35+
console.log(chalk.gray(' Copied client assets to static/'))
36+
37+
// 2. Copy content dir assets (images, etc.) → .vercel/output/static/
38+
if (existsSync(contentDir)) {
39+
await copyContentAssets(contentDir, staticDir)
40+
console.log(chalk.gray(' Copied content assets to static/'))
41+
}
42+
43+
// 3. Copy server bundle → .vercel/output/functions/index.func/
44+
const serverDir = path.resolve(distDir, 'server')
45+
await copyDir(serverDir, funcDir)
46+
console.log(chalk.gray(' Copied server bundle to functions/'))
47+
48+
// 4. Copy HTML template into function dir (not accessible from static/ at runtime)
49+
const templateSrc = path.resolve(clientDir, 'src/server/index.html')
50+
await fs.copyFile(templateSrc, path.resolve(funcDir, 'index.html'))
51+
52+
// 5. Write .vc-config.json
53+
await fs.writeFile(
54+
path.resolve(funcDir, '.vc-config.json'),
55+
JSON.stringify({
56+
runtime: 'nodejs22.x',
57+
handler: 'entry-vercel.js',
58+
launcherType: 'Nodejs',
59+
}, null, 2),
60+
)
61+
62+
// 6. Write config.json
63+
await fs.writeFile(
64+
path.resolve(outputDir, 'config.json'),
65+
JSON.stringify({
66+
version: 3,
67+
routes: [
68+
{ handle: 'filesystem' },
69+
{ src: '/(.*)', dest: '/index' },
70+
],
71+
}, null, 2),
72+
)
73+
74+
console.log(chalk.green('Vercel output generated →'), outputDir)
75+
}
76+
77+
async function copyDir(src: string, dest: string) {
78+
await fs.mkdir(dest, { recursive: true })
79+
const entries = await fs.readdir(src, { withFileTypes: true })
80+
81+
for (const entry of entries) {
82+
const srcPath = path.join(src, entry.name)
83+
const destPath = path.join(dest, entry.name)
84+
85+
if (entry.isDirectory()) {
86+
await copyDir(srcPath, destPath)
87+
} else {
88+
await fs.copyFile(srcPath, destPath)
89+
}
90+
}
91+
}
92+
93+
async function copyContentAssets(contentDir: string, staticDir: string) {
94+
const entries = await fs.readdir(contentDir, { withFileTypes: true })
95+
96+
for (const entry of entries) {
97+
const srcPath = path.join(contentDir, entry.name)
98+
99+
if (entry.isDirectory()) {
100+
const destSubDir = path.join(staticDir, entry.name)
101+
await copyContentAssetsRecursive(srcPath, destSubDir)
102+
} else {
103+
const ext = path.extname(entry.name).toLowerCase()
104+
if (CONTENT_EXTENSIONS.has(ext)) {
105+
await fs.copyFile(srcPath, path.join(staticDir, entry.name))
106+
}
107+
}
108+
}
109+
}
110+
111+
async function copyContentAssetsRecursive(srcDir: string, destDir: string) {
112+
const entries = await fs.readdir(srcDir, { withFileTypes: true })
113+
114+
for (const entry of entries) {
115+
const srcPath = path.join(srcDir, entry.name)
116+
117+
if (entry.isDirectory()) {
118+
await copyContentAssetsRecursive(srcPath, path.join(destDir, entry.name))
119+
} else {
120+
const ext = path.extname(entry.name).toLowerCase()
121+
if (CONTENT_EXTENSIONS.has(ext)) {
122+
await fs.mkdir(destDir, { recursive: true })
123+
await fs.copyFile(srcPath, path.join(destDir, entry.name))
124+
}
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)