feat(examples): worked example for typed api blocks
- Add examples/basic-app/app/api/directory.ts and app/pages/api-block-demo.wrn as the end-to-end worked example for typed api blocks (Task 6). - Add **/*.generated.api-checks.ts to .prettierignore: this generated file must match the CLI's raw output byte-for-byte for check:generated-types, and prettier was reformatting it. - Fix packages/csr/test/api-call.test.ts: no-unsafe-function-type lint error from the raw Function type, uncovered while running the full gate. - Rebuild editors/vscode bundles (packages/compiler and packages/syntax changed in Tasks 1-5). - Regenerate docs/public-api-0.8.json (additive: isIdentPart, isIdentStart, skipLiteralOrComment newly exported from packages/syntax). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// WRN editor language server source hash: afce4195f664e656dc8f38283bda8118637d977cd2eee8ba08c80906681510cd
|
||||
// WRN editor language server source hash: cf98947fd66392200bdfb18524a13e8bbb77d4920ba65a4d724e1124152571a3
|
||||
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||
// @bun @bun-cjs
|
||||
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
||||
@@ -169631,6 +169631,32 @@ var isWs = (c) => c === " " || c === "\t" || c === `
|
||||
` || c === "\r";
|
||||
var isIdentStart = (c) => /[A-Za-z_]/.test(c);
|
||||
var isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
|
||||
function skipLiteralOrComment(src, i, atLineStart) {
|
||||
const c = src[i];
|
||||
if (c === "/" && src[i + 1] === "*") {
|
||||
const close = src.indexOf("*/", i + 2);
|
||||
return close === -1 ? src.length : close + 2;
|
||||
}
|
||||
if (atLineStart && c === "/" && src[i + 1] === "/") {
|
||||
const newline = src.indexOf(`
|
||||
`, i + 2);
|
||||
return newline === -1 ? src.length : newline;
|
||||
}
|
||||
if (c === '"' || c === "'" || c === "`") {
|
||||
let j = i + 1;
|
||||
while (j < src.length) {
|
||||
if (src[j] === "\\") {
|
||||
j += 2;
|
||||
continue;
|
||||
}
|
||||
if (src[j] === c)
|
||||
return j + 1;
|
||||
j++;
|
||||
}
|
||||
return src.length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class Lexer {
|
||||
src;
|
||||
@@ -169892,46 +169918,23 @@ class Lexer {
|
||||
const start = this.pos + 1;
|
||||
let depth = 0;
|
||||
let i = this.pos;
|
||||
let str = null;
|
||||
let atLineStart = false;
|
||||
for (;i < src.length; i++) {
|
||||
while (i < src.length) {
|
||||
const c = src[i];
|
||||
if (str) {
|
||||
if (c === "\\") {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === str)
|
||||
str = null;
|
||||
continue;
|
||||
}
|
||||
if (c === `
|
||||
`) {
|
||||
atLineStart = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === "/" && src[i + 1] === "*") {
|
||||
const close = src.indexOf("*/", i + 2);
|
||||
if (close === -1)
|
||||
break;
|
||||
i = close + 1;
|
||||
const skipped = skipLiteralOrComment(src, i, atLineStart);
|
||||
if (skipped !== null) {
|
||||
i = skipped;
|
||||
atLineStart = false;
|
||||
continue;
|
||||
}
|
||||
if (atLineStart && c === "/" && src[i + 1] === "/") {
|
||||
const newline = src.indexOf(`
|
||||
`, i + 2);
|
||||
if (newline === -1)
|
||||
break;
|
||||
i = newline - 1;
|
||||
continue;
|
||||
}
|
||||
if (c !== " " && c !== "\t" && c !== "\r")
|
||||
atLineStart = false;
|
||||
if (c === '"' || c === "'" || c === "`") {
|
||||
str = c;
|
||||
continue;
|
||||
}
|
||||
if (c === "{")
|
||||
depth++;
|
||||
else if (c === "}") {
|
||||
@@ -169941,6 +169944,7 @@ class Lexer {
|
||||
return src.slice(start, i);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
throw new LexError(`Unbalanced braces starting at offset ${this.pos}`);
|
||||
}
|
||||
@@ -170752,6 +170756,120 @@ var WRN_DIAGNOSTIC_CODES = {
|
||||
migration: "WRN-MIGRATION-001"
|
||||
};
|
||||
|
||||
// packages/syntax/src/api-sections.ts
|
||||
var SECTION_NAMES = ["request", "response", "error"];
|
||||
var REQUEST_SUBSECTION_NAMES = ["parameters", "body"];
|
||||
function scanTopLevelBlocks(source, names) {
|
||||
const found = new Map;
|
||||
const lx = new Lexer(source);
|
||||
let depth = 0;
|
||||
let i = 0;
|
||||
let atLineStart = true;
|
||||
while (i < source.length) {
|
||||
const c = source[i];
|
||||
if (c === `
|
||||
`) {
|
||||
atLineStart = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
const skipped = skipLiteralOrComment(source, i, atLineStart);
|
||||
if (skipped !== null) {
|
||||
i = skipped;
|
||||
atLineStart = false;
|
||||
continue;
|
||||
}
|
||||
if (c !== " " && c !== "\t" && c !== "\r")
|
||||
atLineStart = false;
|
||||
if (depth === 0 && isIdentStart(c)) {
|
||||
let j = i + 1;
|
||||
while (j < source.length && isIdentPart(source[j]))
|
||||
j++;
|
||||
const word = source.slice(i, j);
|
||||
let k = j;
|
||||
let lineStartAtK = false;
|
||||
while (k < source.length) {
|
||||
const kc = source[k];
|
||||
if (kc === " " || kc === "\t" || kc === "\r") {
|
||||
k++;
|
||||
continue;
|
||||
}
|
||||
if (kc === `
|
||||
`) {
|
||||
lineStartAtK = true;
|
||||
k++;
|
||||
continue;
|
||||
}
|
||||
const kSkipped = skipLiteralOrComment(source, k, lineStartAtK);
|
||||
if (kSkipped !== null) {
|
||||
k = kSkipped;
|
||||
lineStartAtK = false;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (names.includes(word) && source[k] === "{") {
|
||||
lx.pos = k;
|
||||
const start = k + 1;
|
||||
const text = lx.readBalancedBraces();
|
||||
if (!found.has(word))
|
||||
found.set(word, { text, start });
|
||||
i = lx.pos;
|
||||
continue;
|
||||
}
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c === "{")
|
||||
depth++;
|
||||
else if (c === "}")
|
||||
depth = Math.max(0, depth - 1);
|
||||
i++;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
function absolutize(span, outer) {
|
||||
if (!span)
|
||||
return;
|
||||
return outer ? { text: span.text, start: outer.start + span.start } : span;
|
||||
}
|
||||
function parseFields(span) {
|
||||
if (!span)
|
||||
return [];
|
||||
const fields = [];
|
||||
let cursor = 0;
|
||||
for (const rawLine of span.text.split(`
|
||||
`)) {
|
||||
const lineOffset = span.start + cursor;
|
||||
cursor += rawLine.length + 1;
|
||||
const line = rawLine.trim().replace(/,$/, "");
|
||||
if (!line || line.startsWith("//"))
|
||||
continue;
|
||||
const match = /^([A-Za-z_$][A-Za-z0-9_$]*)(\?)?\s*:\s*(.+)$/.exec(line);
|
||||
if (!match) {
|
||||
throw new LexError(`Expected "name: type" in an api request section, got "${line}" at offset ${lineOffset}`);
|
||||
}
|
||||
fields.push({ name: match[1], optional: match[2] === "?", type: match[3].trim() });
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
function parseApiSections(source) {
|
||||
const top = scanTopLevelBlocks(source, SECTION_NAMES);
|
||||
if (top.size === 0)
|
||||
return null;
|
||||
const request = top.get("request");
|
||||
const sub = request ? scanTopLevelBlocks(request.text, REQUEST_SUBSECTION_NAMES) : new Map;
|
||||
return {
|
||||
parameters: parseFields(absolutize(sub.get("parameters"), request)),
|
||||
body: parseFields(absolutize(sub.get("body"), request)),
|
||||
response: top.get("response")?.text ?? "",
|
||||
error: top.get("error")?.text ?? ""
|
||||
};
|
||||
}
|
||||
function hasRequestSection(source) {
|
||||
return scanTopLevelBlocks(source, SECTION_NAMES).has("request");
|
||||
}
|
||||
|
||||
// packages/syntax/src/types.ts
|
||||
function runtimeTypeOf(annotation) {
|
||||
if (!annotation)
|
||||
@@ -171632,7 +171750,18 @@ function parse(source) {
|
||||
const method = expect("ident").value.toUpperCase();
|
||||
const path = lx.readPath();
|
||||
const body = lx.readBalancedBraces();
|
||||
dataApis.push({ mode, name: name2, method, path, body });
|
||||
const sections = parseApiSections(body);
|
||||
if (sections && mode !== "client" && hasRequestSection(body)) {
|
||||
throw new ParseError(`An ssr api block cannot declare "request": there is no caller at render time to supply it. Use a client block, or a server function.`);
|
||||
}
|
||||
dataApis.push({
|
||||
mode,
|
||||
name: name2,
|
||||
method,
|
||||
path,
|
||||
body: sections ? "" : body,
|
||||
...sections ? { sections } : {}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "functions": {
|
||||
|
||||
Reference in New Issue
Block a user