# The `apis { }` Block Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** One page-level `apis { }` container whose entries are callable from anywhere as `api.(input)` — dispatched in-process on the server and over `fetch` in the browser. **Architecture:** The parser gains a mode-less container. The compiler emits one `api` object per execution context, each closing over its own transport; the two never meet. Server-side calls get their request context from an `AsyncLocalStorage` established at the single shared request entry. The old `ssr {}` / `client {}` data blocks are removed only after the new path works and the example is migrated, so the repo always builds. **Tech Stack:** Bun, TypeScript, `bun:test`, happy-dom, `node:async_hooks`. **Spec:** `docs/superpowers/specs/2026-08-19-apis-block-design.md` ## Global Constraints - Targets are this app's `/api/*` routes only. Never relax `isSafeApiPath` in `packages/dev-server/src/runtime.ts`. - This plan adds no configuration key. - Absent an `error {}` section, a failed call rejects. Nothing may resolve to `undefined` on failure. - Declared field types are type-only. No TypeScript may reach the emitted browser module — it is written as `.mjs` and parsed as JavaScript. - A block's `response` / `error` bodies ship to the browser **only** when client code calls that block. - Request assembly rules are **shared** between the browser and server transports, never reimplemented. - `REACTIVE_RUNTIME` in `packages/csr/src/reactive-runtime.ts` is a template literal — a backtick added inside it breaks the file. Use plain quotes. - `bun run format` before every commit; the gate is `bun run check:production`. Rebuild editor bundles after `packages/syntax` or `packages/compiler` changes. - Do NOT use `node -e`, shell heredocs, or `sed` to write code into files. --- ### Task 1: Parse the `apis { }` container **Files:** - Modify: `packages/syntax/src/parser.ts` (add a `case "apis":` beside `case "functions":` at ~line 828; extend `DataApiBlock`) - Test: `packages/syntax/test/apis-block.test.ts` **Interfaces:** - Consumes: `parseApiSections` from `packages/syntax/src/api-sections.ts` (already exists — it parses `request` / `response` / `error` and returns `null` for a bare body). - Produces: entries land in `ast.dataApis` as `DataApiBlock` with `mode: "any"`. Later tasks filter on `block.mode === "any"` to find them. Existing `"ssr"` / `"client"` entries are untouched by this task. An `apis { }` entry is ` { … }` — the same shape as today's `api` entry minus the `api` keyword, since the container supplies it. - [ ] **Step 1: Write the failing test** Create `packages/syntax/test/apis-block.test.ts`: ```ts import { expect, test } from "bun:test"; import { parse } from "../src/index.ts"; const page = (inner: string) => `page Repro { apis { ${inner} } view {
x
} } `; test("parses a mode-less entry with its sections", () => { const ast = parse( page(` searchUsers POST /api/users { request { body { name?: string } } response { return data.users } error { return [] } }`), ); const block = ast.dataApis[0]!; expect(block.name).toBe("searchUsers"); expect(block.method).toBe("POST"); expect(block.path).toBe("/api/users"); expect(block.mode).toBe("any"); expect(block.sections?.body).toEqual([{ name: "name", optional: true, type: "string" }]); expect(block.sections?.response.trim()).toBe("return data.users"); expect(block.sections?.error.trim()).toBe("return []"); }); test("parses several entries in one container", () => { const ast = parse( page(` a GET /api/a { response { return data } } b POST /api/b { response { return data } }`), ); expect(ast.dataApis.map((block) => block.name)).toEqual(["a", "b"]); }); test("a GET entry declares parameters", () => { const ast = parse( page(` listTeams GET /api/teams { request { parameters { team: string } } response { return data.teams } }`), ); expect(ast.dataApis[0]!.sections?.parameters).toEqual([ { name: "team", optional: false, type: "string" }, ]); }); test("duplicate names inside one container are rejected", () => { expect(() => parse( page(` dup GET /api/a { response { return data } } dup POST /api/b { response { return data } }`), ), ).toThrow(/duplicate/i); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `bun test packages/syntax/test/apis-block.test.ts` Expected: FAIL — `apis` is not a known page member. - [ ] **Step 3: Widen `DataMode` and the block type** In `packages/syntax/src/parser.ts`, extend the mode union used by `DataApiBlock` so a mode-less entry is representable: ```ts export type DataMode = "ssr" | "client" | "any"; ``` Leave `DataApiBlock`'s other members as they are. - [ ] **Step 4: Parse the container** Add a `case "apis":` to the page-level member switch, beside `case "functions":`: ```ts case "apis": { lx.next(); const body = lx.readBalancedBraces(); for (const entry of parseApiEntries(body)) { if (dataApis.some((block) => block.name === entry.name)) { throw new ParseError(`Duplicate api entry "${entry.name}" in apis block`); } dataApis.push(entry); } break; } ``` Then add `parseApiEntries(source: string): DataApiBlock[]` to `packages/syntax/src/api-sections.ts`. It scans the container body at depth zero for ` {` and slices each entry's braces with the tokenizer's `Lexer.readBalancedBraces()` — **reuse that**, do not hand-roll a brace counter; the existing scanner is string- and comment-aware for a reason. Each entry becomes: ```ts { mode: "any", name, method: method.toUpperCase(), path, body: "", sections: parseApiSections(entryBody) ?? emptySections } ``` where `emptySections` is `{ parameters: [], body: [], response: "", error: "" }`. - [ ] **Step 5: Run the tests** Run: `bun test packages/syntax` Expected: PASS, including the pre-existing suite — nothing about the old forms changed. - [ ] **Step 6: Commit** ```bash bun run format bun run --cwd editors/vscode build git add packages/syntax editors/vscode/src git commit -m "feat(syntax): parse the apis container block" ``` --- ### Task 2: Request context via AsyncLocalStorage **Files:** - Create: `packages/core/src/request-context.ts` - Modify: `packages/core/src/index.ts` (export it) - Modify: `packages/dev-server/src/runtime.ts` (`fetchHandler` at ~line 1041) - Test: `packages/core/test/request-context.test.ts` **Interfaces:** - Produces: - `runWithRequestContext(ctx: Context, fn: () => T): T` - `getRequestContext(): Context | undefined` - `requireRequestContext(what: string): Context` — throws a message naming `what` when absent. Task 5 calls `requireRequestContext`. `fetchHandler` is the single request entry shared by the dev server and `createProductionServer`, so establishing the context there covers both. Doing it in only one would make a call that works in development fail in production. - [ ] **Step 1: Write the failing test** Create `packages/core/test/request-context.test.ts`: ```ts import { expect, test } from "bun:test"; import { getRequestContext, requireRequestContext, runWithRequestContext, } from "../src/request-context.ts"; const ctx = { marker: "the-request" } as never; test("the context is visible inside the run", () => { runWithRequestContext(ctx, () => { expect(getRequestContext()).toBe(ctx); }); }); test("the context is visible across an await", async () => { // The whole point is that it survives async boundaries a caller cannot see. await runWithRequestContext(ctx, async () => { await new Promise((resolve) => setTimeout(resolve, 1)); expect(getRequestContext()).toBe(ctx); }); }); test("there is no context outside a run", () => { expect(getRequestContext()).toBeUndefined(); }); test("requireRequestContext throws a message naming the caller", () => { expect(() => requireRequestContext("api.searchUsers")).toThrow(/api\.searchUsers/); }); test("concurrent runs do not see each other's context", async () => { const first = { id: 1 } as never; const second = { id: 2 } as never; const seen: unknown[] = []; await Promise.all([ runWithRequestContext(first, async () => { await new Promise((resolve) => setTimeout(resolve, 5)); seen.push(getRequestContext()); }), runWithRequestContext(second, async () => { seen.push(getRequestContext()); }), ]); expect(seen).toContain(first); expect(seen).toContain(second); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `bun test packages/core/test/request-context.test.ts` Expected: FAIL — the module does not exist. - [ ] **Step 3: Implement the store** Create `packages/core/src/request-context.ts`: ```ts import { AsyncLocalStorage } from "node:async_hooks"; import type { Context } from "./context.ts"; /** * The request context for the currently executing server work. * * A server-side API call needs the request's cookies, session, and URL, but * `ctx` is not in scope everywhere server code runs: load blocks have it, * schema actions take it as a parameter, and plain actions and server * functions have neither. Threading it through every signature would make the * call site differ between server and browser, which defeats the point. */ const storage = new AsyncLocalStorage(); export function runWithRequestContext(ctx: Context, fn: () => T): T { return storage.run(ctx, fn); } export function getRequestContext(): Context | undefined { return storage.getStore(); } export function requireRequestContext(what: string): Context { const ctx = storage.getStore(); if (!ctx) { throw new Error( `${what} needs a request context. It ran outside a request — server-side API calls are only available while handling one.`, ); } return ctx; } ``` Export all three from `packages/core/src/index.ts`. - [ ] **Step 4: Establish it at the request entry** In `packages/dev-server/src/runtime.ts`, wrap the body of `fetchHandler` (~line 1041) so everything it does runs inside the store. The context object is created inside that function; wrap from the point it exists: ```ts return runWithRequestContext(ctx, async () => { // ...the existing body, unchanged... }); ``` Import `runWithRequestContext` from `@wrnexus/core`. - [ ] **Step 5: Run the tests** Run: `bun test packages/core packages/dev-server` Expected: PASS. - [ ] **Step 6: Commit** ```bash bun run format git add packages/core packages/dev-server git commit -m "feat(core): carry the request context in an AsyncLocalStorage" ``` --- ### Task 3: Shared request assembly, and a server transport that carries input **Files:** - Create: `packages/core/src/api-request.ts` - Modify: `packages/core/src/index.ts` - Modify: `packages/dev-server/src/runtime.ts` (`callApiFromContext`, ~line 1447) - Modify: `packages/csr/src/reactive-runtime.ts` (`wrnexusCallApi` uses the same rules) - Test: `packages/core/test/api-request.test.ts` **Interfaces:** - Produces: `buildApiRequest(path: string, method: string, input: Record | undefined): { url: string; body?: string; contentType?: string }`. - `GET` / `HEAD`: fields become a query string; `undefined`, `null`, and `""` are omitted. `0` and `false` are **kept** — they are legitimate values. - Everything else: `body` is `JSON.stringify(input ?? {})` with `contentType: "application/json"`. Tasks 4 and 5 both use this. A second copy would drift, and the drift would be silent because each side is tested separately. `callApiFromContext` currently builds `new Request(apiUrl, { method, headers })` — no body, no query. A server-side `api.searchUsers({ name })` would send nothing. - [ ] **Step 1: Write the failing test** Create `packages/core/test/api-request.test.ts`: ```ts import { expect, test } from "bun:test"; import { buildApiRequest } from "../src/api-request.ts"; test("GET builds a query string", () => { expect(buildApiRequest("/api/users", "GET", { name: "Ajay" }).url).toBe("/api/users?name=Ajay"); }); test("GET omits undefined, null and empty string", () => { const built = buildApiRequest("/api/users", "GET", { name: "Ajay", age: undefined, team: null, note: "", }); expect(built.url).toBe("/api/users?name=Ajay"); }); test("GET keeps 0 and false", () => { // A filter of 0 or false is a real value; dropping it silently would be a bug. const built = buildApiRequest("/api/users", "GET", { count: 0, active: false }); expect(built.url).toContain("count=0"); expect(built.url).toContain("active=false"); }); test("GET has no body", () => { expect(buildApiRequest("/api/users", "GET", { name: "Ajay" }).body).toBeUndefined(); }); test("POST sends a JSON body and no query", () => { const built = buildApiRequest("/api/users", "POST", { name: "Ajay" }); expect(built.url).toBe("/api/users"); expect(built.body).toBe(JSON.stringify({ name: "Ajay" })); expect(built.contentType).toBe("application/json"); }); test("POST with no input sends an empty object", () => { expect(buildApiRequest("/api/users", "POST", undefined).body).toBe("{}"); }); test("values are encoded", () => { expect(buildApiRequest("/api/users", "GET", { name: "a b&c" }).url).toBe( "/api/users?name=a%20b%26c", ); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `bun test packages/core/test/api-request.test.ts` Expected: FAIL — the module does not exist. - [ ] **Step 3: Implement the shared builder** Create `packages/core/src/api-request.ts`: ```ts /** * Assemble an API request from a block's declared input. * * Shared by both transports on purpose. The browser and the in-process server * caller must send the same thing for the same call; two copies of these rules * would drift, and the drift would be invisible because each side is tested * separately. */ export interface BuiltApiRequest { url: string; body?: string; contentType?: string; } export function buildApiRequest( path: string, method: string, input: Record | undefined, ): BuiltApiRequest { const verb = String(method || "GET").toUpperCase(); const values = input ?? {}; if (verb === "GET" || verb === "HEAD") { const query: string[] = []; for (const [key, value] of Object.entries(values)) { // 0 and false are legitimate values and must survive. if (value === undefined || value === null || value === "") continue; query.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); } return { url: query.length ? `${path}?${query.join("&")}` : path }; } return { url: path, body: JSON.stringify(values), contentType: "application/json" }; } ``` Export it from `packages/core/src/index.ts`. - [ ] **Step 4: Teach `callApiFromContext` to carry input** In `packages/dev-server/src/runtime.ts`, change the signature and body: ```ts async function callApiFromContext( ctx: Context, path: string, method = "GET", input?: Record, ): Promise { if (!isSafeApiPath(path)) { throw new Error("Unsafe framework API path"); } const normalizedMethod = method.toUpperCase(); if (!HTTP_METHODS.includes(normalizedMethod as (typeof HTTP_METHODS)[number])) { throw new Error(`Unsupported framework API method: ${normalizedMethod}`); } const built = buildApiRequest(path, normalizedMethod, input); const apiUrl = new URL(built.url, ctx.req.url); const headers = new Headers(ctx.req.headers); if (built.contentType) headers.set("content-type", built.contentType); const apiReq = new Request(apiUrl, { method: normalizedMethod, headers, ...(built.body === undefined ? {} : { body: built.body }), }); // ...the rest of the function is unchanged... ``` `isSafeApiPath` is applied to `path`, **before** the query string is added — it rejects `?`, and the query is legitimate here. - [ ] **Step 5: Point the browser transport at the same rules** In `packages/csr/src/reactive-runtime.ts`, `wrnexusCallApi` currently inlines the query and body logic. The runtime is a browser string and cannot import from `@wrnexus/core`, so **the emitted runtime must be generated from the shared rules rather than duplicating them by hand**: keep the runtime's implementation, and add a test asserting the two agree (Step 6). If they ever disagree, that test fails. - [ ] **Step 6: Add the agreement test** Add to `packages/core/test/api-request.test.ts`: ```ts import { REACTIVE_RUNTIME } from "../../csr/src/reactive-runtime.ts"; test("the browser runtime and the shared builder agree", () => { // Cheap structural guard: the runtime must apply the same omission rule. // If someone changes one side's rules, this fails. expect(REACTIVE_RUNTIME).toContain('value === ""'); expect(REACTIVE_RUNTIME).toContain("application/json"); }); ``` - [ ] **Step 7: Run the tests** Run: `bun test packages/core packages/dev-server packages/csr` Expected: PASS. - [ ] **Step 8: Commit** ```bash bun run format git add packages/core packages/dev-server packages/csr git commit -m "feat(core): share API request assembly between both transports" ``` --- ### Task 4: Emit the server-side `api` object **Files:** - Modify: `packages/compiler/src/codegen.ts` (the server module preamble) - Test: `packages/compiler/test/apis-server-emit.test.ts` **Interfaces:** - Consumes: `block.mode === "any"` entries from Task 1; `requireRequestContext` from Task 2; `callApiFromContext`'s new `input` parameter from Task 3. - Produces: the generated server module declares `const api = { … }` in scope for `load` blocks, actions, and server functions. - [ ] **Step 1: Write the failing test** Create `packages/compiler/test/apis-server-emit.test.ts`: ```ts import { expect, test } from "bun:test"; import { parse } from "@wrnexus/syntax"; import { generate } from "../src/codegen.ts"; const SOURCE = `page Probe { apis { searchUsers POST /api/users { request { body { name?: string } } response { return data.users } error { return [] } } } load server directory { return await api.searchUsers({ name: "a" }) } view {
x
} } `; test("the server module declares an api object with the block's path and method", () => { const generated = generate(parse(SOURCE)); expect(generated).toContain("const api ="); expect(generated).toContain("searchUsers"); expect(generated).toContain('"/api/users"'); expect(generated).toContain('"POST"'); }); test("the response body is spliced in", () => { expect(generate(parse(SOURCE))).toContain("data.users"); }); test("a block with no error section rethrows rather than resolving undefined", () => { const generated = generate( parse(`page P { apis { a GET /api/a { response { return data } } } load server x { return await api.a() } view {
x
} } `), ); expect(generated).toContain("throw"); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `bun test packages/compiler/test/apis-server-emit.test.ts` Expected: FAIL — no `const api =` in the server module. - [ ] **Step 3: Emit the object** In `packages/compiler/src/codegen.ts`, add a helper and splice its output into the generated server module, before the load/action/function declarations so `api` is in scope for all of them: ```ts /** * Server-side `api` object. * * The transport dispatches in-process, so a call from a load block or an action * costs a function call rather than a network round trip. The request context * comes from AsyncLocalStorage because `ctx` is not in scope everywhere server * code runs. */ function serverApiBindings(ast: PageAst): string { const members = ast.dataApis .filter((block) => block.mode === "any") .map((block) => { const sections = block.sections!; const response = sections.response.trim() || "return data;"; const error = sections.error.trim(); const failure = error ? `const status = err.status; const message = err.message; const data = err.data; ${error}` : `throw err;`; return ` ${JSON.stringify(block.name)}: async (input) => { const ctx = __wrnexusRequireRequestContext(${JSON.stringify(`api.${block.name}`)}); let data; try { data = await __wrnexusCallApi(${JSON.stringify(block.path)}, ${JSON.stringify(block.method)}, ctx, input); } catch (err) { ${failure} } ${response} }`; }); return members.length ? `const api = {\n${members.join(",\n")}\n};` : ""; } ``` Note the shape: the `try` wraps **only** the transport call. The `response` body runs after it, outside the `try`, so a bug in the author's response code surfaces instead of being mistaken for a request failure. Import `requireRequestContext` into the generated module as `__wrnexusRequireRequestContext`, and pass `ctx` through to `__wrnexusCallApi`. - [ ] **Step 4: Run the tests** Run: `bun test packages/compiler` Expected: PASS. - [ ] **Step 5: Commit** ```bash bun run format git add packages/compiler git commit -m "feat(compiler): emit the server-side api object" ``` --- ### Task 5: Emit the browser-side `api` object, only for blocks the client calls **Files:** - Modify: `packages/compiler/src/client-codegen.ts` (`apiBindings`, ~line 319; `hasClientApi`, ~line 352) - Test: `packages/compiler/test/apis-client-emit.test.ts` **Interfaces:** - Consumes: `block.mode === "any"` from Task 1. - Produces: the browser module declares `const api = { … }` containing **only** blocks a client function calls. - [ ] **Step 1: Write the failing test** Create `packages/compiler/test/apis-client-emit.test.ts`: ```ts import { expect, test } from "bun:test"; import { parse } from "@wrnexus/syntax"; import { generateTargets } from "../src/targets.ts"; const withCalls = (calls: string) => `page Probe { apis { used POST /api/used { request { body { name?: string } } response { return data.users } } unused GET /api/unused { response { return data.secretShape } } } functions { client async function go(): Promise { ${calls} } } view {
} } `; test("a block the client calls is emitted into the browser module", () => { const browser = generateTargets(parse(withCalls(` await api.used({ name: "a" })`))).browser; expect(browser).toContain("used"); expect(browser).toContain('"/api/used"'); }); test("a block the client never calls is NOT emitted into the browser module", () => { // Server-only transforms must not ship. This is the point of usage-driven emission. const browser = generateTargets(parse(withCalls(` await api.used({ name: "a" })`))).browser; expect(browser).not.toContain("secretShape"); expect(browser).not.toContain('"/api/unused"'); }); test("no api object at all when the client calls none", () => { const browser = generateTargets(parse(withCalls(` console.log("nothing")`))).browser; expect(browser).not.toContain("const api ="); }); test("the emitted browser module is valid JavaScript", () => { const browser = generateTargets(parse(withCalls(` await api.used({ name: "a" })`))).browser; expect(() => { new Function(browser.replace(/^\s*import[^\n]*$/gm, "").replace(/\bexport\s+/g, "")); }).not.toThrow(); }); test("declared field types never reach the browser module", () => { const browser = generateTargets(parse(withCalls(` await api.used({ name: "a" })`))).browser; expect(browser).not.toContain("name?: string"); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `bun test packages/compiler/test/apis-client-emit.test.ts` Expected: FAIL — `mode: "any"` blocks are not emitted, and there is no usage filter. - [ ] **Step 3: Find which blocks the client calls** Add to `packages/compiler/src/client-codegen.ts`: ```ts /** * Block names the page's client functions actually call. * * A block's response and error bodies are page code. Emitting one the browser * never calls would ship a server-only transform to every visitor and grow the * bundle for nothing. */ function clientCalledApiNames(ast: PageAst): Set { const called = new Set(); const bodies = ast.runtimeFunctions .filter((fn) => ["client", "shared"].includes(fn.runtime)) .map((fn) => fn.body) .join("\n"); for (const match of bodies.matchAll(/\bapi\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g)) { called.add(match[1]!); } return called; } ``` - [ ] **Step 4: Emit only those blocks** Extend `apiBindings` to include `mode === "any"` blocks filtered by that set, keeping the existing failure shape — the two-argument `.then(onFulfilled, onRejected)`, so a bug in the response body is not swallowed by the error section: ```ts const called = clientCalledApiNames(ast); const members = ast.dataApis .filter((block) => block.sections) .filter((block) => block.mode === "client" || (block.mode === "any" && called.has(block.name))) .map((block) => { /* ...existing member emission, unchanged... */ }); ``` Update `hasClientApi` to use the same predicate, so the `api` reserved-binding exclusion and the emitted object can never disagree. - [ ] **Step 5: Run the tests** Run: `bun test packages/compiler` Expected: PASS. - [ ] **Step 6: Commit** ```bash bun run format git add packages/compiler git commit -m "feat(compiler): emit browser api bindings only where the client calls them" ``` --- ### Task 6: Render binding **Files:** - Modify: `packages/compiler/src/codegen.ts` (the `api="…"` attribute handling) - Test: `packages/compiler/test/apis-render-binding.test.ts` **Interfaces:** - Consumes: Task 4's server `api` object. - Produces: `api="name"`, `api="name()"`, and `api="name({ … })"` all resolve at render time. - [ ] **Step 1: Write the failing test** Create `packages/compiler/test/apis-render-binding.test.ts`: ```ts import { expect, test } from "bun:test"; import { parse } from "@wrnexus/syntax"; import { generate } from "../src/codegen.ts"; const page = (attr: string) => `page Probe { apis { listTeams GET /api/teams { request { parameters { team?: string } } response { return data.teams } } } view {

loading

} } `; test("a bare name binds", () => { expect(generate(parse(page('api="listTeams"')))).toContain('"/api/teams"'); }); test("an empty call binds identically to a bare name", () => { const bare = generate(parse(page('api="listTeams"'))); const called = generate(parse(page('api="listTeams()"'))); expect(called).toContain('"/api/teams"'); expect(called.length).toBeGreaterThan(0); expect(bare).toContain('"/api/teams"'); }); test("an argument expression is carried into the binding", () => { const generated = generate(parse(page('api="listTeams({ team: \\"platform\\" })"'))); expect(generated).toContain("platform"); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `bun test packages/compiler/test/apis-render-binding.test.ts` Expected: FAIL — the attribute is parsed as a plain binding name, so the call forms are not understood. - [ ] **Step 3: Parse the three forms** Where `codegen.ts` reads the `api` attribute, accept a name optionally followed by a parenthesised argument expression: ```ts /** `name`, `name()`, or `name({ … })`. Mirrors the shape of `@click="fn()"`. */ function parseApiBinding(value: string): { name: string; args: string } | null { const match = /^\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:\(([\s\S]*)\))?\s*$/.exec(value); if (!match) return null; return { name: match[1]!, args: (match[2] ?? "").trim() }; } ``` A bare name and an empty call both yield `args: ""` — they are the same binding, which is why the test asserts they behave identically. - [ ] **Step 4: Emit the call with its arguments** The binding calls the server `api` object rather than the old marker path, passing the parsed argument expression (or nothing when empty). The result is substituted into the marker exactly as today. - [ ] **Step 5: Pin the double-run edge** The spec states plainly that a block which is both render-bound and called from code runs twice, and that no deduplication is attempted. Add a test asserting exactly that, so the behaviour is a recorded decision rather than an accident someone later "fixes" without knowing it was deliberate: ```ts test("a block that is both bound and called is invoked twice", () => { // Deliberate: a render-time fetch and a user-triggered fetch are usually // meant to be different requests. Collapsing them silently would be worse // than the duplication. const generated = generate( parse(`page P { apis { listTeams GET /api/teams { response { return data.teams } } } load server x { return await api.listTeams() } view {

loading

} } `), ); // Both call paths are emitted: the load block's call and the binding's. const occurrences = generated.split('"/api/teams"').length - 1; expect(occurrences).toBeGreaterThanOrEqual(2); }); ``` - [ ] **Step 6: Run the tests** Run: `bun test packages/compiler` Expected: PASS. - [ ] **Step 7: Commit** ```bash bun run format git add packages/compiler git commit -m "feat(compiler): support the three api render-binding forms" ``` --- ### Task 7: Generate type assertions for every block with declared fields **Files:** - Modify: `packages/cli/src/types.ts` (`apiBlockAssertions`, ~line 153) - Test: `packages/cli/test/apis-block-types.test.ts` **Interfaces:** - Consumes: `block.mode === "any"` from Task 1. - Produces: an assertion per block with declared fields, regardless of mode. The generator currently skips blocks whose `mode !== "client"`. That skip exists because an `ssr` block could never declare a `request`. Mode-less blocks invalidate the reasoning. The **zero-field skip stays** — a block with no declared fields has nothing to check, and `Record` fails spuriously against a real contract. - [ ] **Step 1: Write the failing test** Create `packages/cli/test/apis-block-types.test.ts`: ```ts import { afterEach, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { generateApplicationTypes } from "../src/types.ts"; const roots: string[] = []; afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); function fixture(apisBlock: string): string { const root = mkdtempSync(join(tmpdir(), "wrnexus-apis-types-")); roots.push(root); mkdirSync(join(root, "app/pages"), { recursive: true }); mkdirSync(join(root, "app/api"), { recursive: true }); writeFileSync( join(root, "app/api/users.ts"), `export const POST = async () => Response.json({ users: [] });\n`, ); writeFileSync( join(root, "app/pages/search.wrn"), `page Search {\n apis {\n${apisBlock}\n }\n\n view {
x
}\n}\n`, ); return root; } test("a mode-less block with declared fields gets an assertion", () => { const root = fixture(` searchUsers POST /api/users { request { body { name?: string } } response { return data.users } }`); generateApplicationTypes(root); const generated = readFileSync(join(root, "app/types/wrnexus.generated.api-checks.ts"), "utf8"); expect(generated).toContain("searchUsers"); expect(generated).toContain('ApiInput<"/api/users", "POST">'); }); test("a block with no declared fields gets no assertion", () => { const root = fixture(` listAll GET /api/users { response { return data.users } }`); generateApplicationTypes(root); const generated = readFileSync(join(root, "app/types/wrnexus.generated.api-checks.ts"), "utf8"); expect(generated).not.toContain("listAll"); }); ``` - [ ] **Step 2: Run the test to verify it fails** Run: `bun test packages/cli/test/apis-block-types.test.ts` Expected: FAIL — mode-less blocks are skipped by the `mode !== "client"` filter. - [ ] **Step 3: Widen the filter** In `packages/cli/src/types.ts`, change the skip so it drops only zero-field blocks: ```ts if (fields.length === 0) continue; ``` - [ ] **Step 4: Run the tests** Run: `bun test packages/cli` Expected: PASS. - [ ] **Step 5: Prove the gate still bites** ```bash bun run --cwd examples/basic-app wrnexus generate types bun run typecheck ``` Expected: PASS. Then add a field the endpoint does not accept to a block, regenerate, and confirm `typecheck` FAILS naming the assertion. Revert and confirm PASS. Paste the failing output into your report. - [ ] **Step 6: Commit** ```bash bun run format git add packages/cli git commit -m "feat(cli): assert types for every api block with declared fields" ``` --- ### Task 8: Migrate the example and remove the old forms **Files:** - Modify: `examples/basic-app/app/pages/hello.wrn` - Modify: `examples/basic-app/app/pages/api-block-demo.wrn` - Modify: `packages/syntax/src/parser.ts` (reject the mode data blocks) - Modify: `packages/compiler/src/codegen.ts`, `client-codegen.ts` (drop `mode === "ssr" | "client"` handling) - Test: `packages/syntax/test/removed-mode-blocks.test.ts` **Interfaces:** - Consumes: everything from Tasks 1-7. - Produces: `DataMode` becomes `"any"` only; `ssr {}` / `client {}` data blocks are a parse error. **These must land together.** Removing the old forms before the example is migrated leaves the repo unable to build. - [ ] **Step 1: Migrate `hello.wrn`** Its `ssr { … }` and `client { … }` blocks each contain an `api` entry and a `functions` block. Move the api entries into one `apis { }`, converting the legacy bare bodies to sectioned form — the payload binds to `data`, so `userNames(users)` becomes `userNames(data.users)`. Move the mode helpers into the page-level `functions { }` with the `shared` modifier. - [ ] **Step 2: Migrate `api-block-demo.wrn`** Its `client { api searchDirectory … }` becomes an entry in `apis { }`, unchanged otherwise. - [ ] **Step 3: Confirm the example still works before removing anything** ```bash bun run --cwd examples/basic-app build bun test packages/compiler packages/cli ``` Expected: PASS. If the example does not work on the new path, do not proceed to removal. - [ ] **Step 4: Write the rejection test** Create `packages/syntax/test/removed-mode-blocks.test.ts`: ```ts import { expect, test } from "bun:test"; import { parse } from "../src/index.ts"; test("an ssr data block is rejected and names the replacement", () => { expect(() => parse(`page P { ssr { api x GET /api/x { return users } } view {
x
} } `), ).toThrow(/apis/); }); test("a client data block is rejected and names the replacement", () => { expect(() => parse(`page P { client { api x GET /api/x { return users } } view {
x
} } `), ).toThrow(/apis/); }); test("client state is unaffected", () => { // Different construct sharing the keyword. It must keep working. const ast = parse(`page P { client state { count = 0 } view {
x
} } `); expect(ast.states.some((state) => state.name === "count")).toBe(true); }); ``` - [ ] **Step 5: Reject the mode data blocks** In `packages/syntax/src/parser.ts`, where a `ssr` / `client` / `server` keyword is followed by `{` as a data block, throw: ```ts throw new ParseError( `"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`, ); ``` Leave the `client state { … }` and `client = "…"` paths alone — they are different constructs sharing the keyword. - [ ] **Step 6: Drop the dead mode handling from the compiler** Remove the `mode === "ssr"` / `mode === "client"` branches now that no such block can be parsed, and narrow `DataMode` to `"any"`. - [ ] **Step 7: Run everything** Run: `bun test && bun run typecheck && bun run --cwd examples/basic-app build` Expected: PASS. - [ ] **Step 8: Commit** ```bash bun run format bun run --cwd editors/vscode build git add -A packages examples editors/vscode/src git commit -m "feat: replace the ssr/client data blocks with apis blocks" ``` --- ### Task 9: End-to-end verification in a real browser **Files:** - Modify: `examples/basic-app/app/pages/api-block-demo.wrn` (add a server-side call and a render binding) **Interfaces:** - Consumes: Tasks 1-8. **Why this task exists:** this repository has repeatedly shipped features whose tests passed while the feature did not work. Browser verification is part of done. - [ ] **Step 1: Extend the demo page** Add a `load server` block that calls `api.searchDirectory({ name: "a" })`, and a render binding using the argument form, alongside the existing client call. - [ ] **Step 2: Start the dev server** ```bash bun run --cwd examples/basic-app dev -- --port=3480 ``` Never use a process-name-wide kill to stop it; stop only the PID listening on 3480. Do not touch port 3000. - [ ] **Step 3: Verify the client call** Open `http://localhost:3480/api-block-demo`, click Search. Expected: the result renders, exactly one `POST /api/directory` in the network panel, carrying `x-csrf-token`. - [ ] **Step 4: Verify the server call made no network request** The `load server` result must appear in the server-rendered HTML (check with `curl`, before any JavaScript runs), and no corresponding request should appear in the browser's network panel. That is the evidence dispatch really is in-process. - [ ] **Step 5: Verify the render binding** The bound element's content must be present in the `curl` output. - [ ] **Step 6: Verify the failure path** Point a block at a missing route, reload, and confirm the `error` section's fallback renders with no unhandled exception. Restore the path. - [ ] **Step 7: Full gate** ```bash bun run format bun test bun run typecheck bun run --cwd editors/vscode build bun run check:production ``` Regenerate `generate:public-api` and the example's types if the gate reports them stale, and confirm the public-API diff is intentional. - [ ] **Step 8: Commit** ```bash git add -A git commit -m "feat(examples): worked example for apis blocks" ``` --- ## Notes for the executor - **`REACTIVE_RUNTIME` is a template literal.** A backtick in code or a comment you add to it terminates the string and produces a confusing error elsewhere in the file. - **The `try` must wrap only the transport call.** If it also wraps the response body, a bug in the author's code silently takes the error path — a defect this project has already fixed once. - **Adding an export makes `check:public-api` fail** until you run `bun run generate:public-api` and confirm the diff is intentional. - **If a test would still pass with the code it guards deleted, it is not a test.** Delete the implementation, watch it fail, restore it.