tova 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tova.js +1401 -111
- package/package.json +3 -1
- package/src/analyzer/analyzer.js +831 -709
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +467 -109
- package/src/codegen/client-codegen.js +92 -42
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +290 -36
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +305 -63
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +892 -30
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +491 -1064
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +191 -10
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +549 -6
- package/src/version.js +1 -1
|
@@ -156,7 +156,7 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
156
156
|
const modelDecls = [];
|
|
157
157
|
const aiConfigs = []; // { name: string|null, config: object }
|
|
158
158
|
|
|
159
|
-
const collectFromBody = (stmts, groupPrefix = null, groupMiddlewares = []) => {
|
|
159
|
+
const collectFromBody = (stmts, groupPrefix = null, groupMiddlewares = [], groupVersion = null) => {
|
|
160
160
|
for (const stmt of stmts) {
|
|
161
161
|
if (stmt.type === 'RouteDeclaration') {
|
|
162
162
|
const route = stmt;
|
|
@@ -165,6 +165,7 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
165
165
|
...route,
|
|
166
166
|
path: groupPrefix + route.path,
|
|
167
167
|
_groupMiddlewares: groupMiddlewares.length > 0 ? [...groupMiddlewares] : undefined,
|
|
168
|
+
_version: groupVersion || undefined,
|
|
168
169
|
};
|
|
169
170
|
routes.push(prefixedRoute);
|
|
170
171
|
} else {
|
|
@@ -196,7 +197,8 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
196
197
|
} else if (stmt.type === 'RouteGroupDeclaration') {
|
|
197
198
|
const prefix = groupPrefix ? groupPrefix + stmt.prefix : stmt.prefix;
|
|
198
199
|
const grpMw = [...groupMiddlewares]; // inherit parent group middlewares
|
|
199
|
-
|
|
200
|
+
const ver = stmt.version || groupVersion; // inherit or override version
|
|
201
|
+
collectFromBody(stmt.body, prefix, grpMw, ver);
|
|
200
202
|
} else if (stmt.type === 'RateLimitDeclaration') {
|
|
201
203
|
rateLimitConfig = stmt.config;
|
|
202
204
|
} else if (stmt.type === 'LifecycleHookDeclaration') {
|
|
@@ -240,8 +242,8 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
240
242
|
|
|
241
243
|
// Collect type declarations from shared blocks for model/ORM generation
|
|
242
244
|
const sharedTypes = new Map(); // typeName -> { fields: [{ name, type }] }
|
|
243
|
-
|
|
244
|
-
for (const stmt of
|
|
245
|
+
const _collectTypes = (stmts) => {
|
|
246
|
+
for (const stmt of stmts) {
|
|
245
247
|
if (stmt.type === 'TypeDeclaration' && stmt.variants) {
|
|
246
248
|
const fields = [];
|
|
247
249
|
for (const v of stmt.variants) {
|
|
@@ -254,6 +256,13 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
254
256
|
}
|
|
255
257
|
}
|
|
256
258
|
}
|
|
259
|
+
};
|
|
260
|
+
for (const sb of sharedBlocks) {
|
|
261
|
+
_collectTypes(sb.body);
|
|
262
|
+
}
|
|
263
|
+
// Also collect types from server blocks (for body: Type validation)
|
|
264
|
+
for (const block of serverBlocks) {
|
|
265
|
+
_collectTypes(block.body);
|
|
257
266
|
}
|
|
258
267
|
|
|
259
268
|
// Separate group-only middlewares from global middlewares
|
|
@@ -788,12 +797,12 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
788
797
|
// ════════════════════════════════════════════════════════════
|
|
789
798
|
lines.push('// ── Router ──');
|
|
790
799
|
lines.push('const __routes = [];');
|
|
791
|
-
lines.push('function __addRoute(method, path, handler) {');
|
|
800
|
+
lines.push('function __addRoute(method, path, handler, version) {');
|
|
792
801
|
lines.push(' let pattern = path');
|
|
793
802
|
lines.push(' .replace(/\\*([a-zA-Z_][a-zA-Z0-9_]*)/g, "(?<$1>.+)")');
|
|
794
803
|
lines.push(' .replace(/\\*$/g, "(.*)")');
|
|
795
804
|
lines.push(' .replace(/:([^/]+)/g, "(?<$1>[^/]+)");');
|
|
796
|
-
lines.push(' __routes.push({ method, regex: new RegExp(`^${pattern}$`), handler, _path: path });');
|
|
805
|
+
lines.push(' __routes.push({ method, regex: new RegExp(`^${pattern}$`), handler, _path: path, _version: version || null });');
|
|
797
806
|
lines.push('}');
|
|
798
807
|
lines.push('');
|
|
799
808
|
|
|
@@ -1699,6 +1708,10 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1699
1708
|
const validateDec = decorators.find(d => d.name === 'validate');
|
|
1700
1709
|
const uploadDec = decorators.find(d => d.name === 'upload');
|
|
1701
1710
|
|
|
1711
|
+
// T9-5: Version info for route
|
|
1712
|
+
const routeVersion = route._version;
|
|
1713
|
+
const versionArg = routeVersion ? `, ${JSON.stringify(String(routeVersion.version || ''))}` : '';
|
|
1714
|
+
|
|
1702
1715
|
lines.push(`__addRoute(${JSON.stringify(method)}, ${JSON.stringify(path)}, async (req, params) => {`);
|
|
1703
1716
|
|
|
1704
1717
|
// Auth decorator check
|
|
@@ -1741,6 +1754,68 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1741
1754
|
lines.push(` if (__validationErrors.length > 0) return Response.json({ error: "Validation failed", details: __validationErrors }, { status: 400 });`);
|
|
1742
1755
|
}
|
|
1743
1756
|
|
|
1757
|
+
// T9-1: Route-level body type validation — route POST "/api/users" body: User => handler
|
|
1758
|
+
if (route.bodyType && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1759
|
+
const typeName = route.bodyType.name || (route.bodyType.elementType && route.bodyType.elementType.name);
|
|
1760
|
+
if (typeName && sharedTypes.has(typeName)) {
|
|
1761
|
+
const typeInfo = sharedTypes.get(typeName);
|
|
1762
|
+
if (!uploadDec && !validateDec) {
|
|
1763
|
+
lines.push(` const __body = (await __parseBody(req)) || {};`);
|
|
1764
|
+
}
|
|
1765
|
+
const isArray = route.bodyType.type === 'ArrayTypeAnnotation';
|
|
1766
|
+
if (isArray) {
|
|
1767
|
+
lines.push(` if (!Array.isArray(__body)) return Response.json({ error: "Request body must be an array of ${typeName}" }, { status: 400 });`);
|
|
1768
|
+
lines.push(` const __bodyTypeErrors = [];`);
|
|
1769
|
+
lines.push(` for (let __i = 0; __i < __body.length; __i++) {`);
|
|
1770
|
+
lines.push(` const __item = __body[__i];`);
|
|
1771
|
+
for (const f of typeInfo.fields) {
|
|
1772
|
+
if (f.name === 'id') continue;
|
|
1773
|
+
switch (f.type) {
|
|
1774
|
+
case 'String':
|
|
1775
|
+
lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "string") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a string\`);`);
|
|
1776
|
+
break;
|
|
1777
|
+
case 'Int':
|
|
1778
|
+
lines.push(` if (__item.${f.name} !== undefined && !Number.isInteger(__item.${f.name})) __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be an integer\`);`);
|
|
1779
|
+
break;
|
|
1780
|
+
case 'Float':
|
|
1781
|
+
lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "number") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a number\`);`);
|
|
1782
|
+
break;
|
|
1783
|
+
case 'Bool':
|
|
1784
|
+
lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "boolean") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a boolean\`);`);
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
lines.push(` }`);
|
|
1789
|
+
lines.push(` if (__bodyTypeErrors.length > 0) return Response.json({ error: "Validation failed for ${typeName}[]", details: __bodyTypeErrors }, { status: 400 });`);
|
|
1790
|
+
} else {
|
|
1791
|
+
lines.push(` const __bodyTypeErrors = [];`);
|
|
1792
|
+
for (const f of typeInfo.fields) {
|
|
1793
|
+
if (f.name === 'id') continue;
|
|
1794
|
+
switch (f.type) {
|
|
1795
|
+
case 'String':
|
|
1796
|
+
lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "string") __bodyTypeErrors.push("${f.name} must be a string");`);
|
|
1797
|
+
break;
|
|
1798
|
+
case 'Int':
|
|
1799
|
+
lines.push(` if (__body.${f.name} !== undefined && !Number.isInteger(__body.${f.name})) __bodyTypeErrors.push("${f.name} must be an integer");`);
|
|
1800
|
+
break;
|
|
1801
|
+
case 'Float':
|
|
1802
|
+
lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "number") __bodyTypeErrors.push("${f.name} must be a number");`);
|
|
1803
|
+
break;
|
|
1804
|
+
case 'Bool':
|
|
1805
|
+
lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "boolean") __bodyTypeErrors.push("${f.name} must be a boolean");`);
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
// Check for required fields (non-id fields without defaults)
|
|
1810
|
+
for (const f of typeInfo.fields) {
|
|
1811
|
+
if (f.name === 'id') continue;
|
|
1812
|
+
lines.push(` if (__body.${f.name} === undefined || __body.${f.name} === null) __bodyTypeErrors.push("${f.name} is required");`);
|
|
1813
|
+
}
|
|
1814
|
+
lines.push(` if (__bodyTypeErrors.length > 0) return Response.json({ error: "Validation failed for type ${typeName}", details: __bodyTypeErrors }, { status: 400 });`);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1744
1819
|
// Type-safe body deserialization: if a param has a shared type annotation, auto-validate
|
|
1745
1820
|
if (handlerDecl && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1746
1821
|
for (const p of handlerDecl.params) {
|
|
@@ -1857,9 +1932,65 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1857
1932
|
this._emitHandlerCall(lines, `${handler}(req, params)`, timeoutMs);
|
|
1858
1933
|
}
|
|
1859
1934
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1935
|
+
// T9-3: Detect generator-based streaming — if handler uses yield, wrap in streaming response
|
|
1936
|
+
const isGeneratorHandler = route.handler.type === 'Identifier'
|
|
1937
|
+
? (handlerDecl && this._containsYield(handlerDecl.body))
|
|
1938
|
+
: (route.handler.type === 'FunctionDeclaration' || route.handler.type === 'LambdaExpression')
|
|
1939
|
+
? this._containsYield(route.handler.body || route.handler)
|
|
1940
|
+
: false;
|
|
1941
|
+
|
|
1942
|
+
if (isGeneratorHandler) {
|
|
1943
|
+
lines.push(` if (__result && typeof __result[Symbol.asyncIterator] === "function") {`);
|
|
1944
|
+
lines.push(` const __encoder = new TextEncoder();`);
|
|
1945
|
+
lines.push(` const __stream = new ReadableStream({`);
|
|
1946
|
+
lines.push(` async start(controller) {`);
|
|
1947
|
+
lines.push(` try {`);
|
|
1948
|
+
lines.push(` for await (const __chunk of __result) {`);
|
|
1949
|
+
lines.push(` const __data = typeof __chunk === "string" ? __chunk : JSON.stringify(__chunk);`);
|
|
1950
|
+
lines.push(` controller.enqueue(__encoder.encode(\`data: \${__data}\\n\\n\`));`);
|
|
1951
|
+
lines.push(` }`);
|
|
1952
|
+
lines.push(` } finally { controller.close(); }`);
|
|
1953
|
+
lines.push(` }`);
|
|
1954
|
+
lines.push(` });`);
|
|
1955
|
+
lines.push(` return new Response(__stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" } });`);
|
|
1956
|
+
lines.push(` }`);
|
|
1957
|
+
lines.push(` if (__result && typeof __result[Symbol.iterator] === "function" && typeof __result !== "string") {`);
|
|
1958
|
+
lines.push(` const __encoder = new TextEncoder();`);
|
|
1959
|
+
lines.push(` const __chunks = [...__result];`);
|
|
1960
|
+
lines.push(` const __stream = new ReadableStream({`);
|
|
1961
|
+
lines.push(` start(controller) {`);
|
|
1962
|
+
lines.push(` for (const __chunk of __chunks) {`);
|
|
1963
|
+
lines.push(` const __data = typeof __chunk === "string" ? __chunk : JSON.stringify(__chunk);`);
|
|
1964
|
+
lines.push(` controller.enqueue(__encoder.encode(\`data: \${__data}\\n\\n\`));`);
|
|
1965
|
+
lines.push(` }`);
|
|
1966
|
+
lines.push(` controller.close();`);
|
|
1967
|
+
lines.push(` }`);
|
|
1968
|
+
lines.push(` });`);
|
|
1969
|
+
lines.push(` return new Response(__stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" } });`);
|
|
1970
|
+
lines.push(` }`);
|
|
1971
|
+
}
|
|
1972
|
+
// T9-5: API versioning — add version headers to responses
|
|
1973
|
+
if (routeVersion) {
|
|
1974
|
+
const ver = JSON.stringify(String(routeVersion.version || ''));
|
|
1975
|
+
lines.push(` const __addVersionHeaders = (res) => {`);
|
|
1976
|
+
lines.push(` const h = new Headers(res.headers);`);
|
|
1977
|
+
lines.push(` h.set("API-Version", ${ver});`);
|
|
1978
|
+
if (routeVersion.deprecated) {
|
|
1979
|
+
lines.push(` h.set("Deprecation", "true");`);
|
|
1980
|
+
if (routeVersion.sunset) {
|
|
1981
|
+
lines.push(` h.set("Sunset", ${JSON.stringify(String(routeVersion.sunset))});`);
|
|
1982
|
+
}
|
|
1983
|
+
lines.push(` h.set("Link", '</api/v' + (parseInt(${ver}) + 1) + req.url.replace(/\\/api\\/v\\d+/, "") + '>; rel="successor-version"');`);
|
|
1984
|
+
}
|
|
1985
|
+
lines.push(` return new Response(res.body, { status: res.status, headers: h });`);
|
|
1986
|
+
lines.push(` };`);
|
|
1987
|
+
lines.push(` if (__result instanceof Response) return __addVersionHeaders(__result);`);
|
|
1988
|
+
lines.push(` return __addVersionHeaders(Response.json(__result));`);
|
|
1989
|
+
} else {
|
|
1990
|
+
lines.push(` if (__result instanceof Response) return __result;`);
|
|
1991
|
+
lines.push(` return Response.json(__result);`);
|
|
1992
|
+
}
|
|
1993
|
+
lines.push(`}${versionArg});`);
|
|
1863
1994
|
lines.push('');
|
|
1864
1995
|
}
|
|
1865
1996
|
}
|
|
@@ -1928,46 +2059,66 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1928
2059
|
lines.push(' ],');
|
|
1929
2060
|
}
|
|
1930
2061
|
|
|
1931
|
-
// Request body schema for POST/PUT/PATCH
|
|
1932
|
-
if (['post', 'put', 'patch'].includes(method)
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
lines.push(` ${bp.name}: { "$ref": "#/components/schemas/${ta.name}" },`);
|
|
1941
|
-
} else if (ta) {
|
|
1942
|
-
let jsonType;
|
|
1943
|
-
switch (ta.name) {
|
|
1944
|
-
case 'Int': jsonType = '"integer"'; break;
|
|
1945
|
-
case 'Float': jsonType = '"number"'; break;
|
|
1946
|
-
case 'Bool': jsonType = '"boolean"'; break;
|
|
1947
|
-
default: jsonType = '"string"'; break;
|
|
1948
|
-
}
|
|
1949
|
-
lines.push(` ${bp.name}: { type: ${jsonType} },`);
|
|
2062
|
+
// Request body schema for POST/PUT/PATCH — prefer route-level bodyType, fall back to handler params
|
|
2063
|
+
if (['post', 'put', 'patch'].includes(method)) {
|
|
2064
|
+
if (route.bodyType) {
|
|
2065
|
+
// T9-1: Route-level body type annotation
|
|
2066
|
+
const bt = route.bodyType;
|
|
2067
|
+
if (bt.type === 'ArrayTypeAnnotation' && bt.elementType) {
|
|
2068
|
+
const elName = bt.elementType.name;
|
|
2069
|
+
if (sharedTypes.has(elName)) {
|
|
2070
|
+
lines.push(` requestBody: { required: true, content: { "application/json": { schema: { type: "array", items: { "$ref": "#/components/schemas/${elName}" } } } } },`);
|
|
1950
2071
|
} else {
|
|
1951
|
-
lines.push(`
|
|
2072
|
+
lines.push(` requestBody: { required: true, content: { "application/json": { schema: { type: "array", items: ${tovaTypeToJsonSchema(elName)} } } } },`);
|
|
2073
|
+
}
|
|
2074
|
+
} else if (bt.type === 'TypeAnnotation' && sharedTypes.has(bt.name)) {
|
|
2075
|
+
lines.push(` requestBody: { required: true, content: { "application/json": { schema: { "$ref": "#/components/schemas/${bt.name}" } } } },`);
|
|
2076
|
+
} else if (bt.type === 'TypeAnnotation') {
|
|
2077
|
+
lines.push(` requestBody: { required: true, content: { "application/json": { schema: ${tovaTypeToJsonSchema(bt.name)} } } },`);
|
|
2078
|
+
}
|
|
2079
|
+
} else if (handlerDecl) {
|
|
2080
|
+
const bodyParams = handlerDecl.params.filter(p => p.name !== 'req' && !pathParams.includes(p.name));
|
|
2081
|
+
if (bodyParams.length > 0) {
|
|
2082
|
+
lines.push(' requestBody: {');
|
|
2083
|
+
lines.push(' content: { "application/json": { schema: { type: "object", properties: {');
|
|
2084
|
+
for (const bp of bodyParams) {
|
|
2085
|
+
const ta = bp.typeAnnotation;
|
|
2086
|
+
if (ta && ta.name && sharedTypes.has(ta.name)) {
|
|
2087
|
+
lines.push(` ${bp.name}: { "$ref": "#/components/schemas/${ta.name}" },`);
|
|
2088
|
+
} else if (ta) {
|
|
2089
|
+
let jsonType;
|
|
2090
|
+
switch (ta.name) {
|
|
2091
|
+
case 'Int': jsonType = '"integer"'; break;
|
|
2092
|
+
case 'Float': jsonType = '"number"'; break;
|
|
2093
|
+
case 'Bool': jsonType = '"boolean"'; break;
|
|
2094
|
+
default: jsonType = '"string"'; break;
|
|
2095
|
+
}
|
|
2096
|
+
lines.push(` ${bp.name}: { type: ${jsonType} },`);
|
|
2097
|
+
} else {
|
|
2098
|
+
lines.push(` ${bp.name}: { type: "string" },`);
|
|
2099
|
+
}
|
|
1952
2100
|
}
|
|
2101
|
+
lines.push(' } } } },');
|
|
2102
|
+
lines.push(' },');
|
|
1953
2103
|
}
|
|
1954
|
-
lines.push(' } } } },');
|
|
1955
|
-
lines.push(' },');
|
|
1956
2104
|
}
|
|
1957
2105
|
}
|
|
1958
2106
|
|
|
1959
|
-
// Response schema
|
|
1960
|
-
|
|
1961
|
-
|
|
2107
|
+
// Response schema — prefer route-level responseType (T9-2), fall back to handler return type
|
|
2108
|
+
const responseType = route.responseType || (handlerDecl && handlerDecl.returnType);
|
|
2109
|
+
if (responseType) {
|
|
2110
|
+
const rt = responseType;
|
|
1962
2111
|
if (rt.type === 'ArrayTypeAnnotation' && rt.elementType) {
|
|
1963
2112
|
const elName = rt.elementType.name;
|
|
1964
2113
|
if (sharedTypes.has(elName)) {
|
|
1965
2114
|
lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: { "$ref": "#/components/schemas/${elName}" } } } } } },`);
|
|
1966
2115
|
} else {
|
|
1967
|
-
lines.push(
|
|
2116
|
+
lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: ${tovaTypeToJsonSchema(elName)} } } } } },`);
|
|
1968
2117
|
}
|
|
1969
2118
|
} else if (rt.type === 'TypeAnnotation' && sharedTypes.has(rt.name)) {
|
|
1970
2119
|
lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { "$ref": "#/components/schemas/${rt.name}" } } } } },`);
|
|
2120
|
+
} else if (rt.type === 'TypeAnnotation') {
|
|
2121
|
+
lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: ${tovaTypeToJsonSchema(rt.name)} } } } },`);
|
|
1971
2122
|
} else {
|
|
1972
2123
|
lines.push(' responses: { "200": { description: "Success" } },');
|
|
1973
2124
|
}
|
|
@@ -1994,6 +2145,32 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1994
2145
|
lines.push('');
|
|
1995
2146
|
}
|
|
1996
2147
|
|
|
2148
|
+
// T9-5: API versions endpoint — list available versions
|
|
2149
|
+
const versionedRoutes = routes.filter(r => r._version);
|
|
2150
|
+
if (versionedRoutes.length > 0) {
|
|
2151
|
+
const versionMap = new Map();
|
|
2152
|
+
for (const r of versionedRoutes) {
|
|
2153
|
+
const v = String(r._version.version || '');
|
|
2154
|
+
if (!versionMap.has(v)) {
|
|
2155
|
+
versionMap.set(v, { version: v, deprecated: !!r._version.deprecated, sunset: r._version.sunset || null });
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
lines.push('// ── API Versions ──');
|
|
2159
|
+
lines.push('__addRoute("GET", "/api/versions", async () => {');
|
|
2160
|
+
lines.push(' return Response.json({');
|
|
2161
|
+
lines.push(' versions: [');
|
|
2162
|
+
for (const [, info] of versionMap) {
|
|
2163
|
+
const parts = [`version: ${JSON.stringify(info.version)}`];
|
|
2164
|
+
if (info.deprecated) parts.push('deprecated: true');
|
|
2165
|
+
if (info.sunset) parts.push(`sunset: ${JSON.stringify(info.sunset)}`);
|
|
2166
|
+
lines.push(` { ${parts.join(', ')} },`);
|
|
2167
|
+
}
|
|
2168
|
+
lines.push(' ]');
|
|
2169
|
+
lines.push(' });');
|
|
2170
|
+
lines.push('});');
|
|
2171
|
+
lines.push('');
|
|
2172
|
+
}
|
|
2173
|
+
|
|
1997
2174
|
// Include __contains helper if needed
|
|
1998
2175
|
if (this._needsContainsHelper) {
|
|
1999
2176
|
lines.push(this.getContainsHelper());
|
|
@@ -2527,7 +2704,31 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2527
2704
|
|
|
2528
2705
|
for (const block of testBlocks) {
|
|
2529
2706
|
const name = block.name || 'Tests';
|
|
2707
|
+
const blockTimeout = block.timeout || null;
|
|
2530
2708
|
lines.push(`describe(${JSON.stringify(name)}, () => {`);
|
|
2709
|
+
|
|
2710
|
+
// Emit beforeEach if defined
|
|
2711
|
+
if (block.beforeEach && block.beforeEach.length > 0) {
|
|
2712
|
+
lines.push(' beforeEach(async () => {');
|
|
2713
|
+
this.pushScope();
|
|
2714
|
+
for (const s of block.beforeEach) {
|
|
2715
|
+
lines.push(' ' + this.generateStatement(s));
|
|
2716
|
+
}
|
|
2717
|
+
this.popScope();
|
|
2718
|
+
lines.push(' });');
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// Emit afterEach if defined
|
|
2722
|
+
if (block.afterEach && block.afterEach.length > 0) {
|
|
2723
|
+
lines.push(' afterEach(async () => {');
|
|
2724
|
+
this.pushScope();
|
|
2725
|
+
for (const s of block.afterEach) {
|
|
2726
|
+
lines.push(' ' + this.generateStatement(s));
|
|
2727
|
+
}
|
|
2728
|
+
this.popScope();
|
|
2729
|
+
lines.push(' });');
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2531
2732
|
for (const stmt of block.body) {
|
|
2532
2733
|
if (stmt.type === 'FunctionDeclaration') {
|
|
2533
2734
|
const fnName = stmt.name;
|
|
@@ -2539,9 +2740,10 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2539
2740
|
}
|
|
2540
2741
|
const body = this.genBlockBody(stmt.body);
|
|
2541
2742
|
this.popScope();
|
|
2743
|
+
const timeoutArg = blockTimeout ? `, ${blockTimeout}` : '';
|
|
2542
2744
|
lines.push(` test(${JSON.stringify(displayName)}, async () => {`);
|
|
2543
2745
|
lines.push(body);
|
|
2544
|
-
lines.push(
|
|
2746
|
+
lines.push(` }${timeoutArg});`);
|
|
2545
2747
|
} else {
|
|
2546
2748
|
lines.push(' ' + this.generateStatement(stmt));
|
|
2547
2749
|
}
|
|
@@ -2553,6 +2755,58 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2553
2755
|
return lines.join('\n');
|
|
2554
2756
|
}
|
|
2555
2757
|
|
|
2758
|
+
generateBench(benchBlocks) {
|
|
2759
|
+
const lines = [];
|
|
2760
|
+
lines.push('// ── Tova Benchmark Runner ──');
|
|
2761
|
+
lines.push('');
|
|
2762
|
+
lines.push('async function __runBench(name, fn, runs) {');
|
|
2763
|
+
lines.push(' runs = runs || 100;');
|
|
2764
|
+
lines.push(' // Warmup');
|
|
2765
|
+
lines.push(' for (let i = 0; i < Math.min(10, runs); i++) await fn();');
|
|
2766
|
+
lines.push(' const times = [];');
|
|
2767
|
+
lines.push(' for (let i = 0; i < runs; i++) {');
|
|
2768
|
+
lines.push(' const start = performance.now();');
|
|
2769
|
+
lines.push(' await fn();');
|
|
2770
|
+
lines.push(' times.push(performance.now() - start);');
|
|
2771
|
+
lines.push(' }');
|
|
2772
|
+
lines.push(' times.sort((a, b) => a - b);');
|
|
2773
|
+
lines.push(' const sum = times.reduce((a, b) => a + b, 0);');
|
|
2774
|
+
lines.push(' const mean = sum / times.length;');
|
|
2775
|
+
lines.push(' const p50 = times[Math.floor(times.length * 0.5)];');
|
|
2776
|
+
lines.push(' const p99 = times[Math.floor(times.length * 0.99)];');
|
|
2777
|
+
lines.push(' console.log(`bench ${JSON.stringify(name)}: mean=${mean.toFixed(2)}ms p50=${p50.toFixed(2)}ms p99=${p99.toFixed(2)}ms (${runs} runs)`);');
|
|
2778
|
+
lines.push('}');
|
|
2779
|
+
lines.push('');
|
|
2780
|
+
lines.push('(async () => {');
|
|
2781
|
+
|
|
2782
|
+
for (const block of benchBlocks) {
|
|
2783
|
+
const name = block.name || 'Benchmark';
|
|
2784
|
+
lines.push(` console.log("── ${name.replace(/"/g, '\\"')} ──");`);
|
|
2785
|
+
for (const stmt of block.body) {
|
|
2786
|
+
if (stmt.type === 'FunctionDeclaration') {
|
|
2787
|
+
const fnName = stmt.name;
|
|
2788
|
+
const displayName = fnName.replace(/_/g, ' ');
|
|
2789
|
+
this.pushScope();
|
|
2790
|
+
for (const p of (stmt.params || [])) {
|
|
2791
|
+
const pName = typeof p === 'string' ? p : (p.name || p.identifier);
|
|
2792
|
+
if (pName) this.declareVar(pName);
|
|
2793
|
+
}
|
|
2794
|
+
const body = this.genBlockBody(stmt.body);
|
|
2795
|
+
this.popScope();
|
|
2796
|
+
lines.push(` await __runBench(${JSON.stringify(displayName)}, async () => {`);
|
|
2797
|
+
lines.push(body);
|
|
2798
|
+
lines.push(' });');
|
|
2799
|
+
} else {
|
|
2800
|
+
lines.push(' ' + this.generateStatement(stmt));
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
lines.push('');
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
lines.push('})();');
|
|
2807
|
+
return lines.join('\n');
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2556
2810
|
_getAiRuntime() {
|
|
2557
2811
|
return `// AI Client Runtime
|
|
2558
2812
|
function __createAI(config) {
|