feat: Prepare Vercel deployment with serverless adapter

- Extract buildApp() from server.ts into app.ts for reuse
- Create api/index.ts as Vercel serverless function entry point
- Add vercel.json with rewrites (static assets + catch-all to API)
- Build script now copies static assets to public/ and templates to dist/
- Include src/templates/** in serverless function bundle
- Local dev and tests continue to work unchanged (60/60 passing)

https://claude.ai/code/session_01CvrcMDqfCKWV2hC3xpRbx3
This commit is contained in:
Claude 2026-03-09 00:18:29 +00:00
parent e966049169
commit 014c4b3bf5
No known key found for this signature in database
6 changed files with 100 additions and 52 deletions

View File

@ -2,4 +2,5 @@ node_modules/
dist/
.env
src/public/css/app.css
/public/
*.tsbuildinfo

View File

@ -0,0 +1,22 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { buildApp } from "../src/app.js";
let appReady: ReturnType<typeof buildApp> | null = null;
function getApp() {
if (!appReady) {
appReady = buildApp().then(async (app) => {
await app.ready();
return app;
});
}
return appReady;
}
export default async function handler(
req: IncomingMessage,
res: ServerResponse,
) {
const app = await getApp();
app.server.emit("request", req, res);
}

View File

@ -7,9 +7,10 @@
"dev": "concurrently \"npm run dev:server\" \"npm run dev:css\"",
"dev:server": "tsx watch --env-file=.env src/server.ts",
"dev:css": "npx @tailwindcss/cli -i src/styles/input.css -o src/public/css/app.css --watch",
"build": "npm run build:css && npm run build:server",
"build": "npm run build:css && npm run build:server && npm run build:assets",
"build:server": "tsc",
"build:css": "npx @tailwindcss/cli -i src/styles/input.css -o src/public/css/app.css --minify",
"build:assets": "cp -r src/public/* public/ 2>/dev/null; cp -r src/templates dist/templates",
"start": "node --env-file=.env dist/server.js",
"test": "vitest run",
"test:watch": "vitest"

View File

@ -0,0 +1,56 @@
import Fastify from "fastify";
import fastifyView from "@fastify/view";
import fastifyStatic from "@fastify/static";
import fastifyFormbody from "@fastify/formbody";
import nunjucks from "nunjucks";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { config } from "./config/index.js";
import { homeRoutes } from "./routes/home.js";
import { flowRoutes } from "./routes/flow.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export async function buildApp() {
const app = Fastify({
logger: {
level: config.nodeEnv === "production" ? "info" : "debug",
transport:
config.nodeEnv === "development"
? { target: "pino-pretty", options: { translateTime: "HH:MM:ss" } }
: undefined,
},
});
// Parse form body (application/x-www-form-urlencoded)
await app.register(fastifyFormbody);
// Serve static assets (local dev only — Vercel serves from public/)
await app.register(fastifyStatic, {
root: join(__dirname, "public"),
prefix: "/",
});
// Nunjucks template engine
nunjucks.configure(join(__dirname, "templates"), {
autoescape: true,
noCache: config.nodeEnv === "development",
});
await app.register(fastifyView, {
engine: { nunjucks },
templates: join(__dirname, "templates"),
options: {
onConfigure: (_env: nunjucks.Environment) => {
// Add any custom filters here
},
},
});
// Register routes
await app.register(homeRoutes);
await app.register(flowRoutes);
return app;
}

View File

@ -1,57 +1,8 @@
import Fastify from "fastify";
import fastifyView from "@fastify/view";
import fastifyStatic from "@fastify/static";
import fastifyFormbody from "@fastify/formbody";
import nunjucks from "nunjucks";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { config } from "./config/index.js";
import { homeRoutes } from "./routes/home.js";
import { flowRoutes } from "./routes/flow.js";
import { buildApp } from "./app.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = await buildApp();
const app = Fastify({
logger: {
level: config.nodeEnv === "production" ? "info" : "debug",
transport:
config.nodeEnv === "development"
? { target: "pino-pretty", options: { translateTime: "HH:MM:ss" } }
: undefined,
},
});
// Parse form body (application/x-www-form-urlencoded)
await app.register(fastifyFormbody);
// Serve static assets
await app.register(fastifyStatic, {
root: join(__dirname, "public"),
prefix: "/",
});
// Nunjucks template engine
const nunjucksEnv = nunjucks.configure(join(__dirname, "templates"), {
autoescape: true,
noCache: config.nodeEnv === "development",
});
await app.register(fastifyView, {
engine: { nunjucks },
templates: join(__dirname, "templates"),
options: {
onConfigure: (env: nunjucks.Environment) => {
// Add any custom filters here
},
},
});
// Register routes
await app.register(homeRoutes);
await app.register(flowRoutes);
// Start server
try {
const address = await app.listen({ port: config.port, host: config.host });
app.log.info(`Jus IA Start Kit running at ${address}`);

View File

@ -0,0 +1,17 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "npm run build",
"outputDirectory": "public",
"functions": {
"api/index.ts": {
"includeFiles": "src/templates/**"
}
},
"rewrites": [
{ "source": "/css/(.*)", "destination": "/css/$1" },
{ "source": "/js/(.*)", "destination": "/js/$1" },
{ "source": "/images/(.*)", "destination": "/images/$1" },
{ "source": "/favicon.ico", "destination": "/favicon.ico" },
{ "source": "/(.*)", "destination": "/api/index" }
]
}