diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 9b43b71..eb1a9e5 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -2,6 +2,8 @@ import type { AppConfig, AuthService, Feature, JobHandler, ServerContext } from import { createApp, createJobQueue, loadConfig } from "@stc/core"; import { createCourseRepository } from "@stc/course-format"; import { connect, migrate } from "@stc/db"; +import { importFeature } from "@stc/feature-import"; +import { resolve } from "node:path"; import indexHtml from "../../web/index.html"; // Temporary: the learner package replaces this with real sessions. @@ -9,17 +11,21 @@ const anonymousAuth: AuthService = { userFromRequest: async () => null }; export async function startServer( config: AppConfig, - features: Feature[], + features: (Feature | ((ctx: ServerContext) => Feature))[], _webFeatures?: unknown, ): Promise<{ server: Bun.Server; stop(): Promise }> { const db = await connect(config.databaseUrl); await migrate(db); const courses = await createCourseRepository(config.courseDirs); + // Feature factories receive the context before the job queue exists (the queue is built from their handlers), so they + // must only touch ctx.jobs while a job or request runs. + const ctx = { config, db, courses, auth: anonymousAuth } as ServerContext; + const resolved = features.map((f) => (typeof f === "function" ? f(ctx) : f)); const handlers: Record = {}; - for (const f of features) Object.assign(handlers, f.jobs); + for (const f of resolved) Object.assign(handlers, f.jobs); const jobs = createJobQueue(db, handlers); - const ctx: ServerContext = { config, db, courses, jobs, auth: anonymousAuth }; - const app = createApp(ctx, features); + ctx.jobs = jobs; + const app = createApp(ctx, resolved); jobs.start(); const server = Bun.serve({ port: config.port, @@ -39,6 +45,9 @@ export async function startServer( if (import.meta.main) { const config = loadConfig(); - const { server } = await startServer(config, []); + // Contract request (import.md): core config should include /courses itself. + const generated = resolve(config.dataDir, "courses"); + if (!config.courseDirs.some((c) => resolve(c) === generated)) config.courseDirs.push(generated); + const { server } = await startServer(config, [(ctx) => importFeature(ctx)]); console.log(`STCourses listening on ${server.url}`); } diff --git a/apps/web/src/features.ts b/apps/web/src/features.ts index 8b077c9..2764c20 100644 --- a/apps/web/src/features.ts +++ b/apps/web/src/features.ts @@ -1,4 +1,5 @@ import type { WebFeature } from "@stc/contracts"; +import { importWeb } from "../../../packages/feature-import/src/web"; // Phase 1 packages append one import line each and push into this list. -export const webFeatures: WebFeature[] = []; +export const webFeatures: WebFeature[] = [importWeb]; diff --git a/packages/feature-import/package.json b/packages/feature-import/package.json index 0b65795..c758331 100644 --- a/packages/feature-import/package.json +++ b/packages/feature-import/package.json @@ -4,5 +4,6 @@ "private": true, "type": "module", "license": "AGPL-3.0-only", - "main": "src/index.ts" + "main": "src/index.ts", + "exports": { ".": "./src/index.ts", "./web": "./src/web.ts" } } diff --git a/packages/feature-import/src/index.ts b/packages/feature-import/src/index.ts index bc9d734..1d7874a 100644 --- a/packages/feature-import/src/index.ts +++ b/packages/feature-import/src/index.ts @@ -1,4 +1,4 @@ -import type { Feature, JobHandler, ServerContext, WebFeature } from "@stc/contracts"; +import type { Feature, JobHandler, ServerContext } from "@stc/contracts"; import type { Deps, ImportOptions } from "./context"; import { importCodeRepo } from "./jobs/code-repo"; import { importPacktVideo } from "./jobs/packt-video"; @@ -34,7 +34,4 @@ export function importFeature(ctx: ServerContext, opts: ImportOptions = {}): Fea }; } -export const importWeb: WebFeature = { - name: "import", - routes: [{ path: "/admin/import", element: () => import("./web/ImportPage"), nav: { label: "Import", area: "admin" } }], -}; +export { importWeb } from "./web"; diff --git a/packages/feature-import/src/web.ts b/packages/feature-import/src/web.ts new file mode 100644 index 0000000..369fa85 --- /dev/null +++ b/packages/feature-import/src/web.ts @@ -0,0 +1,7 @@ +import type { WebFeature } from "@stc/contracts"; + +/** Browser-safe entry (no server imports): apps/web imports this as "@stc/feature-import/web". */ +export const importWeb: WebFeature = { + name: "import", + routes: [{ path: "/admin/import", element: () => import("./web/ImportPage"), nav: { label: "Import", area: "admin" } }], +};