import { expect, test } from "bun:test"; import { migrateApisBlock } from "../src/migrations/apis-block.ts"; const SOURCE = `page Search { client { api searchUsers POST /api/users { request { body { name?: string } } response { return data.users } error { return [] } } } view {
x
} } `; test("a client api entry moves into an apis block", () => { const result = migrateApisBlock(SOURCE) as { source: string; changed: boolean }; expect(result.changed).toBe(true); expect(result.source).toContain("apis {"); expect(result.source).toContain("searchUsers POST /api/users"); expect(result.source).not.toContain("client {\n api"); }); test("the sections survive unchanged", () => { const result = migrateApisBlock(SOURCE) as { source: string }; expect(result.source).toContain("return data.users"); expect(result.source).toContain("return []"); }); test("running it on migrated source changes nothing", () => { const once = (migrateApisBlock(SOURCE) as { source: string }).source; const twice = migrateApisBlock(once) as { source: string; changed: boolean }; expect(twice.changed).toBe(false); expect(twice.source).toBe(once); }); test("a name declared in both modes is skipped with a reason", () => { const clash = `page P { ssr { api dup GET /api/a { response { return data } } } client { api dup GET /api/a { response { return data } } } view {
x
} } `; const result = migrateApisBlock(clash) as { skip: string }; expect(result.skip).toContain("dup"); }); test("a page with an existing apis block merges legacy entries instead of producing two apis blocks", () => { const source = `page P { apis { existing GET /api/existing { response { return data } } } client { api added POST /api/added { response { return data.value } } } view {
x
} } `; const result = migrateApisBlock(source) as { source: string; changed: boolean }; expect(result.changed).toBe(true); const apisCount = (result.source.match(/\bapis\s*\{/g) ?? []).length; expect(apisCount).toBe(1); expect(result.source).toContain("existing GET /api/existing"); expect(result.source).toContain("added POST /api/added"); expect(result.source).not.toContain("client {"); }); test("a mode block mixing api entries with other content is skipped, not partially rewritten", () => { // A bare `ssr { … }` block is invalid syntax once api entries are removed // (only `state` and hydrate forms survive) -- leftover content such as // `functions { }` belongs to the move-mode-functions migration, so this // file cannot be completed by this migration alone. const source = `page P { ssr { api foo GET /api/foo { response { return data } } functions { function helper() { return 1 } } } view {
x
} } `; const result = migrateApisBlock(source) as { skip: string }; expect(result.skip).toContain("ssr"); expect(result.skip).toContain("functions"); }); test("a legacy bare-body api entry is left alone instead of producing a bogus parse failure", () => { // No request/response/error sections -- this is the legacy bare-body form // that `report-legacy-api-bodies` handles for manual review. Folding it // into `apis { }` as-is would produce source the grammar rejects, so this // migration must leave it untouched rather than attempt the rewrite. const source = `page Hello { ssr { api ssrUsers GET /api/users/ssr { return userNames(users) } } view {
x
} } `; const result = migrateApisBlock(source) as { source: string; changed: boolean }; expect(result.changed).toBe(false); expect(result.source).toBe(source); }); test("a brace inside a string literal in a section body does not truncate the entry", () => { const source = `page P { client { api foo GET /api/foo { response { return { text: "a } b" } } } } view {
x
} } `; const result = migrateApisBlock(source) as { source: string; changed: boolean }; expect(result.changed).toBe(true); expect(result.source).toContain('text: "a } b"'); expect(result.source).toContain("apis {"); });