import type { AppConfig, Feature, FeatureFactory } from "@stc/contracts"; import { composeServer, loadConfig, type ComposeOptions } from "@stc/core"; import indexHtml from "../../web/index.html"; import { packageMigrations, productFeatures } from "./features"; export interface StartOptions extends ComposeOptions {} /** * Composes the features (core `composeServer`) and serves them with the SPA. Bun routes win over `fetch`; * "/api/*" falls through to the composed app, so unknown API paths are 404 rather than the SPA. */ export async function startServer(config: AppConfig, features: (Feature | FeatureFactory)[], opts: StartOptions = {}) { const composed = await composeServer(config, features, opts); const { app } = composed; let server: Bun.Server; try { server = Bun.serve({ port: config.port, routes: { ...app.routes, "/api/*": (req) => app.fetch(req), "/*": indexHtml }, fetch: (req) => app.fetch(req), // Production (NODE_ENV=production, as in the Docker image) bundles the SPA once, minified, without HMR. development: process.env.NODE_ENV !== "production", }); } catch (e) { await composed.stop(); throw e; } return { server, ctx: composed.ctx, features: composed.features, async stop() { await server.stop(true); await composed.stop(); }, }; } if (import.meta.main) { const config = loadConfig(); const { server } = await startServer(config, productFeatures(), { migrations: packageMigrations }); console.log(`STCourses listening on ${server.url}`); }