Merge pull request #5 from bipprojectbali/nico/26-feb-26/fix-seed-2

Fix Gambar
This commit is contained in:
2026-03-12 15:21:17 +08:00
committed by GitHub
2 changed files with 206 additions and 209 deletions

View File

@@ -12,7 +12,7 @@
"test": "bun test __tests__/api", "test": "bun test __tests__/api",
"test:ui": "bun test --ui __tests__/api", "test:ui": "bun test --ui __tests__/api",
"test:e2e": "bun run build && playwright test", "test:e2e": "bun run build && playwright test",
"build": "bun run scripts/build.ts", "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*' && cp -r public/* dist/ 2>/dev/null || true",
"start": "NODE_ENV=production bun src/index.ts", "start": "NODE_ENV=production bun src/index.ts",
"seed": "bun prisma/seed.ts" "seed": "bun prisma/seed.ts"
}, },

View File

@@ -12,243 +12,240 @@ const isProduction = process.env.NODE_ENV === "production";
// Auto-seed database in production (ensure admin user exists) // Auto-seed database in production (ensure admin user exists)
if (isProduction && process.env.ADMIN_EMAIL) { if (isProduction && process.env.ADMIN_EMAIL) {
try { try {
console.log("🌱 Running database seed in production..."); console.log("🌱 Running database seed in production...");
const { runSeed } = await import("../prisma/seed.ts"); const { runSeed } = await import("../prisma/seed.ts");
await runSeed(); await runSeed();
} catch (error) { } catch (error) {
console.error("⚠️ Production seed failed:", error); console.error("⚠️ Production seed failed:", error);
// Don't crash the server if seed fails // Don't crash the server if seed fails
} }
} }
const app = new Elysia().use(api); const app = new Elysia().use(api);
if (!isProduction) { if (!isProduction) {
// Development: Use Vite middleware // Development: Use Vite middleware
const { createVite } = await import("./vite"); const { createVite } = await import("./vite");
const vite = await createVite(); const vite = await createVite();
// Serve PWA/TWA assets in dev (root and nested path support) // Serve PWA/TWA assets in dev (root and nested path support)
const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath); const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath);
app.post("/__open-in-editor", ({ body }) => { app.post("/__open-in-editor", ({ body }) => {
const { relativePath, lineNumber, columnNumber } = body as { const { relativePath, lineNumber, columnNumber } = body as {
relativePath: string; relativePath: string;
lineNumber: number; lineNumber: number;
columnNumber: number; columnNumber: number;
}; };
openInEditor(relativePath, { openInEditor(relativePath, {
line: lineNumber, line: lineNumber,
column: columnNumber, column: columnNumber,
editor: "antigravity", editor: "antigravity",
}); });
return { ok: true }; return { ok: true };
}); });
// Vite middleware for other requests // Vite middleware for other requests
app.all("*", async ({ request }) => { app.all("*", async ({ request }) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
// Serve transformed index.html for root or any path that should be handled by the SPA // Serve transformed index.html for root or any path that should be handled by the SPA
if ( if (
pathname === "/" || pathname === "/" ||
(!pathname.includes(".") && (!pathname.includes(".") &&
!pathname.startsWith("/@") && !pathname.startsWith("/@") &&
!pathname.startsWith("/inspector") && !pathname.startsWith("/inspector") &&
!pathname.startsWith("/__open-stack-frame-in-editor") && !pathname.startsWith("/__open-stack-frame-in-editor") &&
!pathname.startsWith("/api")) !pathname.startsWith("/api"))
) { ) {
try { try {
const htmlPath = path.resolve("src/index.html"); const htmlPath = path.resolve("src/index.html");
let html = fs.readFileSync(htmlPath, "utf-8"); let html = fs.readFileSync(htmlPath, "utf-8");
html = await vite.transformIndexHtml(pathname, html); html = await vite.transformIndexHtml(pathname, html);
return new Response(html, { return new Response(html, {
headers: { "Content-Type": "text/html" }, headers: { "Content-Type": "text/html" },
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
} }
return new Promise<Response>((resolve) => { return new Promise<Response>((resolve) => {
// Use a Proxy to mock Node.js req because Bun's Request is read-only // Use a Proxy to mock Node.js req because Bun's Request is read-only
const req = new Proxy(request, { const req = new Proxy(request, {
get(target, prop) { get(target, prop) {
if (prop === "url") return pathname + url.search; if (prop === "url") return pathname + url.search;
if (prop === "method") return request.method; if (prop === "method") return request.method;
if (prop === "headers") if (prop === "headers")
return Object.fromEntries(request.headers as any); return Object.fromEntries(request.headers as any);
return (target as any)[prop]; return (target as any)[prop];
}, },
}) as any; }) as any;
const res = { const res = {
statusCode: 200, statusCode: 200,
setHeader(name: string, value: string) { setHeader(name: string, value: string) {
this.headers[name.toLowerCase()] = value; this.headers[name.toLowerCase()] = value;
}, },
getHeader(name: string) { getHeader(name: string) {
return this.headers[name.toLowerCase()]; return this.headers[name.toLowerCase()];
}, },
writeHead(code: number, headers: Record<string, string>) { writeHead(code: number, headers: Record<string, string>) {
this.statusCode = code; this.statusCode = code;
Object.assign(this.headers, headers); Object.assign(this.headers, headers);
}, },
write(chunk: any, callback?: () => void) { write(chunk: any, callback?: () => void) {
// Collect chunks for streaming responses // Collect chunks for streaming responses
if (!this._chunks) this._chunks = []; if (!this._chunks) this._chunks = [];
this._chunks.push(chunk); this._chunks.push(chunk);
if (callback) callback(); if (callback) callback();
return true; // Indicate we can accept more data return true; // Indicate we can accept more data
}, },
headers: {} as Record<string, string>, headers: {} as Record<string, string>,
end(data: any) { end(data: any) {
// Handle potential Buffer or string data from Vite // Handle potential Buffer or string data from Vite
let body = data; let body = data;
// If we have collected chunks from write() calls, combine them
// If we have collected chunks from write() calls, combine them if (this._chunks && this._chunks.length > 0) {
if (this._chunks && this._chunks.length > 0) { body = Buffer.concat(this._chunks);
body = Buffer.concat(this._chunks); }
} if (data instanceof Uint8Array) {
body = data;
if (data instanceof Uint8Array) { } else if (typeof data === "string") {
body = data; body = data;
} else if (typeof data === "string") { } else if (data) {
body = data; body = String(data);
} else if (data) { }
body = String(data);
}
resolve( resolve(
new Response(body || "", { new Response(body || "", {
status: this.statusCode, status: this.statusCode,
headers: this.headers, headers: this.headers,
}), }),
); );
}, },
// Minimal event emitter mock // Minimal event emitter mock
once() { once() {
return this; return this;
}, },
on() { on() {
return this; return this;
}, },
emit() { emit() {
return this; return this;
}, },
removeListener() { removeListener() {
return this; return this;
}, },
} as any; } as any;
vite.middlewares(req, res, (err: any) => { vite.middlewares(req, res, (err: any) => {
if (err) { if (err) {
console.error("Vite middleware error:", err); console.error("Vite middleware error:", err);
resolve(new Response(err.stack || err.toString(), { status: 500 })); resolve(new Response(err.stack || err.toString(), { status: 500 }));
return; return;
} }
// If Vite doesn't handle it, return 404 // If Vite doesn't handle it, return 404
resolve(new Response("Not Found", { status: 404 })); resolve(new Response("Not Found", { status: 404 }));
}); });
}); });
}); });
} else { } else {
// Production: Final catch-all for static files and SPA fallback // Production: Final catch-all for static files and SPA fallback
app.all("*", async ({ request }) => { app.all("*", async ({ request }) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
// 1. Try exact match in dist // 1. Try exact match in dist
let filePath = path.join( let filePath = path.join(
"dist", "dist",
pathname === "/" ? "index.html" : pathname, pathname === "/" ? "index.html" : pathname,
); );
// 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build) // 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build)
if (isProduction) { if (isProduction) {
const srcPath = path.join("src", pathname); const srcPath = path.join("src", pathname);
if (fs.existsSync(srcPath)) { if (fs.existsSync(srcPath)) {
filePath = srcPath; filePath = srcPath;
} }
// Check public folder for static assets // Check public folder for static assets
const publicPath = path.join("public", pathname); const publicPath = path.join("public", pathname);
if (fs.existsSync(publicPath)) { if (fs.existsSync(publicPath)) {
filePath = publicPath; filePath = publicPath;
} }
} }
// 2. If not found and looks like an asset (has extension), try root of dist or src // 2. If not found and looks like an asset (has extension), try root of dist or src
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
if (pathname.includes(".") && !pathname.endsWith("/")) { if (pathname.includes(".") && !pathname.endsWith("/")) {
const filename = path.basename(pathname); const filename = path.basename(pathname);
// Try root of dist // Try root of dist
const fallbackDistPath = path.join("dist", filename); const fallbackDistPath = path.join("dist", filename);
if ( if (
fs.existsSync(fallbackDistPath) && fs.existsSync(fallbackDistPath) &&
fs.statSync(fallbackDistPath).isFile() fs.statSync(fallbackDistPath).isFile()
) { ) {
filePath = fallbackDistPath; filePath = fallbackDistPath;
} }
// Try public folder // Try public folder
else { else {
const fallbackPublicPath = path.join("public", filename); const fallbackPublicPath = path.join("public", filename);
if ( if (
fs.existsSync(fallbackPublicPath) && fs.existsSync(fallbackPublicPath) &&
fs.statSync(fallbackPublicPath).isFile() fs.statSync(fallbackPublicPath).isFile()
) { ) {
filePath = fallbackPublicPath; filePath = fallbackPublicPath;
} }
} }
// Special handling for PWA files in src // Special handling for PWA files in src
if (pathname.includes("assetlinks.json")) { if (pathname.includes("assetlinks.json")) {
const srcFilename = pathname.includes("assetlinks.json") const srcFilename = pathname.includes("assetlinks.json")
? ".well-known/assetlinks.json" ? ".well-known/assetlinks.json"
: filename; : filename;
const fallbackSrcPath = path.join("src", srcFilename); const fallbackSrcPath = path.join("src", srcFilename);
if ( if (
fs.existsSync(fallbackSrcPath) && fs.existsSync(fallbackSrcPath) &&
fs.statSync(fallbackSrcPath).isFile() fs.statSync(fallbackSrcPath).isFile()
) { ) {
filePath = fallbackSrcPath; filePath = fallbackSrcPath;
} }
} }
} }
} }
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const file = Bun.file(filePath); const file = Bun.file(filePath);
return new Response(file, { return new Response(file, {
headers: { headers: {
Vary: "Accept-Encoding", Vary: "Accept-Encoding",
}, },
}); });
} }
// 3. SPA Fallback: Serve index.html // 3. SPA Fallback: Serve index.html
const indexHtml = path.join("dist", "index.html"); const indexHtml = path.join("dist", "index.html");
if (fs.existsSync(indexHtml)) { if (fs.existsSync(indexHtml)) {
return new Response(Bun.file(indexHtml), { return new Response(Bun.file(indexHtml), {
headers: { headers: {
Vary: "Accept-Encoding", Vary: "Accept-Encoding",
}, },
}); });
} }
return new Response("Not Found", { status: 404 }); return new Response("Not Found", { status: 404 });
}); });
} }
app.listen(PORT); app.listen(PORT);
console.log( console.log(
`🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`, `🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`,
); );
export type ApiApp = typeof app; export type ApiApp = typeof app;