diff --git a/apps/server/scripts/build-vercel.sh b/apps/server/scripts/build-vercel.sh index 64754b48d..53cb533ad 100755 --- a/apps/server/scripts/build-vercel.sh +++ b/apps/server/scripts/build-vercel.sh @@ -7,13 +7,13 @@ set -euo pipefail # - api/[[...route]].js is committed to the repo (Vercel detects it pre-build) # - esbuild bundles server/index.ts → api/_handler.js (self-contained bundle) # - The committed .js wrapper re-exports from _handler.js at runtime -# - Studio SPA is built and copied to public/ for serving the UI +# - Studio SPA is built and copied to api/_studio/ for serving at /_studio # - External dependencies installed in api/node_modules/ (no symlinks) # # Steps: # 1. Build the project with turbo (includes studio) # 2. Bundle the API serverless function (→ api/_handler.js) -# 3. Copy studio dist files to public/ for UI serving +# 3. Copy studio dist files to api/_studio/ for UI serving at /_studio # 4. Install external deps in api/node_modules/ (resolve pnpm symlinks) echo "[build-vercel] Starting server build..." @@ -27,13 +27,13 @@ cd apps/server # 2. Bundle API serverless function node scripts/bundle-api.mjs -# 3. Copy studio dist files to public/ for UI serving -echo "[build-vercel] Copying studio dist to public/..." -rm -rf public -mkdir -p public +# 3. Copy studio dist files to api/_studio/ for UI serving at /_studio +echo "[build-vercel] Copying studio dist to api/_studio/..." +rm -rf api/_studio +mkdir -p api/_studio if [ -d "../studio/dist" ]; then - cp -r ../studio/dist/* public/ - echo "[build-vercel] ✓ Copied studio dist to public/" + cp -r ../studio/dist/* api/_studio/ + echo "[build-vercel] ✓ Copied studio dist to api/_studio/" else echo "[build-vercel] ⚠ Studio dist not found (skipped)" fi @@ -60,4 +60,4 @@ rm package.json cd .. echo "[build-vercel] ✓ External dependencies installed in api/node_modules/" -echo "[build-vercel] Done. Static files in public/, serverless function in api/[[...route]].js → api/_handler.js" +echo "[build-vercel] Done. Studio files in api/_studio/, serverless function in api/[[...route]].js → api/_handler.js" diff --git a/apps/server/server/index.ts b/apps/server/server/index.ts index 2a1cb2e7a..a77a5bd40 100644 --- a/apps/server/server/index.ts +++ b/apps/server/server/index.ts @@ -10,8 +10,11 @@ import { ObjectKernel } from '@objectstack/runtime'; import { createHonoApp } from '@objectstack/hono'; import { getRequestListener } from '@hono/node-server'; +import { serveStatic } from '@hono/node-server/serve-static'; import type { Hono } from 'hono'; import stackConfig from '../objectstack.config'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; // --------------------------------------------------------------------------- // Singleton state — persists across warm Vercel invocations @@ -69,6 +72,33 @@ async function ensureApp(): Promise { const kernel = await ensureKernel(); _app = createHonoApp({ kernel, prefix: '/api/v1' }); + + // Serve studio at /_studio + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const studioPath = join(__dirname, '_studio'); + + // Serve static files from /_studio + _app.get('/_studio/*', serveStatic({ + root: __dirname, + rewriteRequestPath: (path) => { + // Rewrite /_studio/assets/x.js -> /_studio/assets/x.js + return path; + } + })); + + // SPA fallback for studio + _app.get('/_studio/*', serveStatic({ + root: __dirname, + rewriteRequestPath: () => '/_studio/index.html' + })); + + // Serve studio index at /_studio root + _app.get('/_studio', serveStatic({ + root: __dirname, + rewriteRequestPath: () => '/_studio/index.html' + })); + return _app; } diff --git a/apps/server/vercel.json b/apps/server/vercel.json index 84811d0b0..36b0edf1f 100644 --- a/apps/server/vercel.json +++ b/apps/server/vercel.json @@ -12,7 +12,7 @@ "functions": { "api/**/*.js": { "maxDuration": 60, - "includeFiles": "api/node_modules/**" + "includeFiles": "api/{node_modules,_studio}/**" } }, "headers": [ @@ -24,7 +24,6 @@ } ], "rewrites": [ - { "source": "/api/:path*", "destination": "/api/[[...route]]" }, - { "source": "/((?!api/).*)", "destination": "/index.html" } + { "source": "/api/:path*", "destination": "/api/[[...route]]" } ] }