feat: replace the ssr/client data blocks with apis blocks

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 07:59:37 +05:30
co-authored by Claude Opus 5
parent de99a2c2e0
commit 890d6106b3
14 changed files with 135 additions and 1421 deletions
-137
View File
@@ -1,137 +0,0 @@
import { expect, test } from "bun:test";
import { parse } from "../src/index.ts";
const page = (inner: string) => `page Repro {
client {
${inner}
}
view { <main>x</main> }
}
`;
test("parses a sectioned api block into request, response and error", () => {
const ast = parse(
page(` api searchUsers POST /api/users {
request {
body {
name?: string
age?: number
}
}
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.sections?.body).toEqual([
{ name: "name", optional: true, type: "string" },
{ name: "age", optional: true, type: "number" },
]);
expect(block.sections?.response.trim()).toBe("return data.users");
expect(block.sections?.error.trim()).toBe("return []");
});
test("a bare body still parses as the legacy response body", () => {
const ast = parse(
page(` api legacyUsers GET /api/users {
return users.length
}`),
);
const block = ast.dataApis[0]!;
expect(block.sections).toBeUndefined();
expect(block.body.trim()).toBe("return users.length");
});
test("GET parameters are parsed as required when not marked optional", () => {
const ast = parse(
page(` api listUsers GET /api/users {
request {
parameters {
team: string
}
}
response {
return data.users
}
}`),
);
expect(ast.dataApis[0]!.sections?.parameters).toEqual([
{ name: "team", optional: false, type: "string" },
]);
});
test("request inside an ssr block is rejected with a message naming the restriction", () => {
const source = `page Repro {
ssr {
api ssrUsers GET /api/users {
request {
parameters {
team: string
}
}
response {
return data.users
}
}
}
view { <main>x</main> }
}
`;
expect(() => parse(source)).toThrow(/ssr[\s\S]*request/i);
});
test("a brace inside a string literal in the response body does not truncate the section", () => {
const ast = parse(
page(` api searchUsers POST /api/users {
request {
body {
name?: string
}
}
response {
return "a } weird string"
}
error {
return []
}
}`),
);
const block = ast.dataApis[0]!;
expect(block.sections?.response.trim()).toBe('return "a } weird string"');
expect(block.sections?.error.trim()).toBe("return []");
});
test("a legacy block whose comment or string mentions a section keyword stays legacy", () => {
const ast = parse(
page(` api legacyUsers GET /api/users {
// fall back to a manual request { } if this fails
return "response { not a section }"
}`),
);
const block = ast.dataApis[0]!;
expect(block.sections).toBeUndefined();
expect(block.body.trim()).toBe(
'// fall back to a manual request { } if this fails\n return "response { not a section }"',
);
});
+1 -17
View File
@@ -74,28 +74,12 @@ test("a bare body inside apis {} is a parse error naming response", () => {
expect(() => parse(page(` bare GET /api/z { return data }`))).toThrow(/response/i);
});
test("apis entry followed by an ssr api of the same name is rejected", () => {
test("two apis entries of the same name across separate blocks are rejected", () => {
const src = `page Repro {
apis {
foo GET /api/foo { response { return data } }
}
ssr {
api foo GET /api/foo { return data }
}
view { <main>x</main> }
}
`;
expect(() => parse(src)).toThrow(/duplicate/i);
});
test("ssr api followed by an apis entry of the same name is rejected", () => {
const src = `page Repro {
ssr {
api foo GET /api/foo { return data }
}
apis {
foo GET /api/foo { response { return data } }
}
@@ -0,0 +1,33 @@
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 { <main>x</main> }
}
`),
).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 { <main>x</main> }
}
`),
).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 { <main>x</main> }
}
`);
expect(ast.states.some((state) => state.name === "count")).toBe(true);
});