import { readdir, rm, writeFile } from "node:fs/promises"; import { join, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { APP } from "../src/shared/app"; import { validateVersion } from "./bump-version-logic"; const projectRoot = fileURLToPath(new URL("..", import.meta.url)); const distWebDir = join(projectRoot, "dist/web"); const buildDir = join(projectRoot, ".build"); const executablePath = join(projectRoot, `dist/${APP.name}`); const packageJsonPath = join(projectRoot, "package.json"); async function build() { try { await viteBuild(); await codeGeneration(); await bunCompile(); await cleanup(); console.log(`Built executable: ${executablePath}`); } catch (error) { await cleanup(); console.error("Build failed:", error); process.exit(1); } } async function bunCompile() { console.log("Step 4/4: Bun compile..."); await rm(executablePath, { force: true }); const target = process.env["BUN_TARGET"] ?? process.env["BUILD_TARGET"]; const result = await Bun.build({ compile: target ? { autoloadBunfig: true, autoloadDotenv: true, outfile: executablePath, target: target as Bun.Build.CompileTarget, } : { autoloadBunfig: true, autoloadDotenv: true, outfile: executablePath, }, entrypoints: [join(buildDir, "server-entry.ts")], minify: true, sourcemap: "linked", }); if (!result.success) { console.error("Bun compile failed:", result.logs); await cleanup(); process.exit(1); } } async function cleanup() { await rm(buildDir, { force: true, recursive: true }); } async function codeGeneration() { console.log("Step 2/4: Code generation..."); await rm(buildDir, { force: true, recursive: true }); await Bun.write(join(buildDir, ".gitkeep"), ""); const packageJson = (await Bun.file(packageJsonPath).json()) as { version: string }; const version = packageJson.version; if (typeof version !== "string") { console.error("package.json does not have a valid version field"); process.exit(1); } validateVersion(version); await generateMigrationsData(); console.log("Step 3/4: Generating static assets..."); const allFiles = await scanDir(distWebDir, "/"); const importLines: string[] = []; const fileEntries: string[] = []; let indexHtmlVar = ""; for (let i = 0; i < allFiles.length; i++) { const urlPath = allFiles[i]!; const varName = `f${i}`; const filePath = toImportSpecifier(buildDir, join(distWebDir, urlPath.slice(1))); importLines.push(`import ${varName} from "./${filePath}" with { type: "file" };`); if (urlPath === "/index.html") { indexHtmlVar = varName; } else { fileEntries.push(` "${urlPath}": Bun.file(${varName}),`); } } if (!indexHtmlVar) { console.error("index.html not found in dist/web/"); process.exit(1); } const staticAssetsTs = [ `import type { StaticAssets } from "../src/server/static";`, "", ...importLines, "", `export const staticAssets: StaticAssets = {`, ` files: {`, ...fileEntries, ` },`, ` indexHtml: Bun.file(${indexHtmlVar}),`, `};`, "", ].join("\n"); await writeFile(join(buildDir, "static-assets.ts"), staticAssetsTs); const serverEntryTs = [ `import { bootstrap } from "../src/server/bootstrap";`, `import { parseRuntimeArgs } from "../src/server/config";`, `import { createConsoleFallback } from "../src/server/logger";`, `import { MIGRATIONS } from "./migrations-data";`, `import { staticAssets } from "./static-assets";`, "", `const APP_VERSION = "${version}" as const;`, "", `async function main() {`, ` const { configPath } = parseRuntimeArgs();`, ` await bootstrap({ configPath, migrations: MIGRATIONS, mode: "production", staticAssets, version: APP_VERSION });`, `}`, "", `void main().catch((error) => {`, ` createConsoleFallback().fatal(\`启动失败: \${error instanceof Error ? error.message : String(error)}\`);`, ` process.exit(1);`, `});`, "", ].join("\n"); await writeFile(join(buildDir, "server-entry.ts"), serverEntryTs); } async function generateMigrationsData() { const { createHash } = await import("node:crypto"); const { readdirSync, readFileSync } = await import("node:fs"); const migrationsDir = join(projectRoot, "drizzle"); let entries: string[]; try { entries = readdirSync(migrationsDir) .filter((f) => f.endsWith(".sql")) .sort(); } catch { entries = []; } const records = entries.map((filename) => { const sql = readFileSync(join(migrationsDir, filename), "utf-8"); const id = filename.replace(/\.sql$/, ""); const checksum = createHash("sha256").update(sql).digest("hex").slice(0, 16); return { checksum, id, sql: sql.trim() }; }); const lines = [ `import type { MigrationRecord } from "../src/server/db/load-migrations";`, ``, `export const MIGRATIONS: MigrationRecord[] = [`, ...records.map( (r) => ` { id: ${JSON.stringify(r.id)}, sql: ${JSON.stringify(r.sql)}, checksum: ${JSON.stringify(r.checksum)} },`, ), `];`, ``, ].join("\n"); await writeFile(join(buildDir, "migrations-data.ts"), lines); console.log(`Embedded ${records.length} migration(s)`); } async function scanDir(dir: string, prefix: string): Promise { const entries = await readdir(dir, { withFileTypes: true }); const paths: string[] = []; for (const entry of entries) { const fullPath = join(dir, entry.name); const urlPath = `${prefix}${entry.name}`; if (entry.isDirectory()) { paths.push(...(await scanDir(fullPath, `${urlPath}/`))); } else { paths.push(urlPath); } } return paths; } function toImportSpecifier(fromDir: string, targetPath: string) { return relative(fromDir, targetPath).split(sep).join("/"); } async function viteBuild() { console.log("Step 1/3: Vite build..."); const proc = Bun.spawn(["bunx", "--bun", "vite", "build"], { cwd: projectRoot, stderr: "inherit", stdout: "inherit", }); const exitCode = await proc.exited; if (exitCode !== 0) { console.error("Vite build failed"); process.exit(1); } } await build();