feat(learner): JS/TS exercise runner in a Web Worker with timeout and network blocked
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
752e44696f
commit
9ff5862382
@@ -7,6 +7,7 @@ import { libraryRoutes } from "./catalog";
|
||||
import { lessonRoutes } from "./lesson";
|
||||
import { userDataRoutes } from "./userdata";
|
||||
import { codeRoutes } from "./code";
|
||||
import { transpileRoutes } from "./transpile";
|
||||
|
||||
/**
|
||||
* The learner feature: auth, library, reader, notes, progress and exercises.
|
||||
@@ -30,6 +31,7 @@ export function learnerFeature(ctx: ServerContext): Feature & { auth: AuthServic
|
||||
...lessonRoutes(ctx),
|
||||
...userDataRoutes(ctx),
|
||||
...codeRoutes(ctx),
|
||||
...transpileRoutes(),
|
||||
};
|
||||
// Every handler waits for the learner migrations (a no-op once Phase 2 runs them in migrate()).
|
||||
for (const methods of Object.values(routes)) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Feature } from "@stc/contracts";
|
||||
import { badRequest, body, json } from "./http";
|
||||
|
||||
const MAX = 200_000;
|
||||
const ts = new Bun.Transpiler({ loader: "ts", target: "browser" });
|
||||
|
||||
/** Type-strips TypeScript for the in-browser runner (the worker itself runs plain JavaScript). */
|
||||
export function transpileTs(code: string): string {
|
||||
return ts.transformSync(code);
|
||||
}
|
||||
|
||||
export function transpileRoutes(): Feature["routes"] {
|
||||
return {
|
||||
"/api/learner/transpile": {
|
||||
POST: async (req) => {
|
||||
const b = await body<{ code?: string }>(req);
|
||||
if (!b || typeof b.code !== "string") return badRequest("code is required");
|
||||
if (b.code.length > MAX) return badRequest("code is too long");
|
||||
try {
|
||||
return json({ code: transpileTs(b.code) });
|
||||
} catch (e) {
|
||||
return json({ error: (e as Error).message }, 422);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user