tova 0.5.1 → 0.8.2

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.
Files changed (60) hide show
  1. package/bin/tova.js +261 -60
  2. package/package.json +1 -1
  3. package/src/analyzer/analyzer.js +351 -11
  4. package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
  5. package/src/analyzer/deploy-analyzer.js +44 -0
  6. package/src/analyzer/form-analyzer.js +113 -0
  7. package/src/analyzer/scope.js +2 -2
  8. package/src/codegen/base-codegen.js +1160 -10
  9. package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
  10. package/src/codegen/codegen.js +119 -28
  11. package/src/codegen/deploy-codegen.js +49 -0
  12. package/src/codegen/edge-codegen.js +1351 -0
  13. package/src/codegen/form-codegen.js +553 -0
  14. package/src/codegen/security-codegen.js +5 -5
  15. package/src/codegen/server-codegen.js +88 -7
  16. package/src/codegen/shared-codegen.js +5 -0
  17. package/src/codegen/wasm-codegen.js +6 -0
  18. package/src/config/edit-toml.js +6 -2
  19. package/src/config/git-resolver.js +128 -0
  20. package/src/config/lock-file.js +57 -0
  21. package/src/config/module-cache.js +58 -0
  22. package/src/config/module-entry.js +37 -0
  23. package/src/config/module-path.js +31 -0
  24. package/src/config/pkg-errors.js +62 -0
  25. package/src/config/resolve.js +17 -0
  26. package/src/config/resolver.js +139 -0
  27. package/src/config/search.js +28 -0
  28. package/src/config/semver.js +72 -0
  29. package/src/config/toml.js +48 -5
  30. package/src/deploy/deploy.js +217 -0
  31. package/src/deploy/infer.js +218 -0
  32. package/src/deploy/provision.js +311 -0
  33. package/src/diagnostics/error-codes.js +1 -1
  34. package/src/docs/generator.js +1 -1
  35. package/src/formatter/formatter.js +4 -4
  36. package/src/lexer/tokens.js +12 -2
  37. package/src/lsp/server.js +483 -1
  38. package/src/parser/ast.js +60 -5
  39. package/src/parser/{client-ast.js → browser-ast.js} +3 -3
  40. package/src/parser/{client-parser.js → browser-parser.js} +42 -15
  41. package/src/parser/concurrency-ast.js +15 -0
  42. package/src/parser/concurrency-parser.js +236 -0
  43. package/src/parser/deploy-ast.js +37 -0
  44. package/src/parser/deploy-parser.js +132 -0
  45. package/src/parser/edge-ast.js +83 -0
  46. package/src/parser/edge-parser.js +262 -0
  47. package/src/parser/form-ast.js +80 -0
  48. package/src/parser/form-parser.js +206 -0
  49. package/src/parser/parser.js +82 -14
  50. package/src/parser/select-ast.js +39 -0
  51. package/src/registry/plugins/browser-plugin.js +30 -0
  52. package/src/registry/plugins/concurrency-plugin.js +32 -0
  53. package/src/registry/plugins/deploy-plugin.js +33 -0
  54. package/src/registry/plugins/edge-plugin.js +32 -0
  55. package/src/registry/register-all.js +8 -2
  56. package/src/runtime/ssr.js +2 -2
  57. package/src/stdlib/inline.js +38 -6
  58. package/src/stdlib/runtime-bridge.js +152 -0
  59. package/src/version.js +1 -1
  60. package/src/registry/plugins/client-plugin.js +0 -30
@@ -0,0 +1,1351 @@
1
+ // Edge/serverless code generator for the Tova language
2
+ // Produces deployment-ready code for Cloudflare Workers, Deno Deploy, Vercel Edge, AWS Lambda, or Bun.
3
+
4
+ import { createRequire } from 'module';
5
+ import { BaseCodegen } from './base-codegen.js';
6
+
7
+ const _require = createRequire(import.meta.url);
8
+ let _SecurityCodegen;
9
+
10
+ const DEFAULT_TARGET = 'cloudflare';
11
+
12
+ export class EdgeCodegen extends BaseCodegen {
13
+
14
+ /**
15
+ * Merge all EdgeBlock nodes with the same name into a single config.
16
+ */
17
+ static mergeEdgeBlocks(edgeBlocks) {
18
+ let target = DEFAULT_TARGET;
19
+ const routes = [];
20
+ const functions = [];
21
+ const middlewares = [];
22
+ const bindings = { kv: [], sql: [], storage: [], queue: [] };
23
+ const envVars = [];
24
+ const secrets = [];
25
+ const schedules = [];
26
+ const consumers = [];
27
+ const miscStatements = [];
28
+ let healthPath = null;
29
+ let healthChecks = null;
30
+ let corsConfig = null;
31
+ let errorHandler = null;
32
+
33
+ for (const block of edgeBlocks) {
34
+ for (const stmt of block.body) {
35
+ switch (stmt.type) {
36
+ case 'EdgeConfigField':
37
+ if (stmt.key === 'target' && stmt.value.type === 'StringLiteral') {
38
+ target = stmt.value.value;
39
+ }
40
+ break;
41
+ case 'EdgeKVDeclaration':
42
+ bindings.kv.push(stmt);
43
+ break;
44
+ case 'EdgeSQLDeclaration':
45
+ bindings.sql.push(stmt);
46
+ break;
47
+ case 'EdgeStorageDeclaration':
48
+ bindings.storage.push(stmt);
49
+ break;
50
+ case 'EdgeQueueDeclaration':
51
+ bindings.queue.push(stmt);
52
+ break;
53
+ case 'EdgeEnvDeclaration':
54
+ envVars.push(stmt);
55
+ break;
56
+ case 'EdgeSecretDeclaration':
57
+ secrets.push(stmt);
58
+ break;
59
+ case 'EdgeScheduleDeclaration':
60
+ schedules.push(stmt);
61
+ break;
62
+ case 'EdgeConsumeDeclaration':
63
+ consumers.push(stmt);
64
+ break;
65
+ case 'RouteDeclaration':
66
+ routes.push(stmt);
67
+ break;
68
+ case 'MiddlewareDeclaration':
69
+ middlewares.push(stmt);
70
+ break;
71
+ case 'FunctionDeclaration':
72
+ functions.push(stmt);
73
+ break;
74
+ case 'HealthCheckDeclaration':
75
+ healthPath = stmt.path;
76
+ if (stmt.checks && stmt.checks.length > 0) {
77
+ if (!healthChecks) healthChecks = [];
78
+ healthChecks.push(...stmt.checks);
79
+ }
80
+ break;
81
+ case 'CorsDeclaration':
82
+ corsConfig = stmt.config;
83
+ break;
84
+ case 'ErrorHandlerDeclaration':
85
+ errorHandler = stmt;
86
+ break;
87
+ default:
88
+ miscStatements.push(stmt);
89
+ break;
90
+ }
91
+ }
92
+ }
93
+
94
+ return { target, routes, functions, middlewares, bindings, envVars, secrets, schedules, consumers, miscStatements, healthPath, healthChecks, corsConfig, errorHandler };
95
+ }
96
+
97
+ /**
98
+ * Generate edge function code for the given target.
99
+ * @param {Object} config — merged config from mergeEdgeBlocks
100
+ * @param {string} sharedCode — shared/top-level compiled code
101
+ * @returns {string} — complete edge function JS
102
+ */
103
+ generate(config, sharedCode, securityConfig = null) {
104
+ const { target } = config;
105
+ switch (target) {
106
+ case 'cloudflare': return this._generateCloudflare(config, sharedCode, securityConfig);
107
+ case 'deno': return this._generateDeno(config, sharedCode, securityConfig);
108
+ case 'vercel': return this._generateVercel(config, sharedCode, securityConfig);
109
+ case 'lambda': return this._generateLambda(config, sharedCode, securityConfig);
110
+ case 'bun': return this._generateBun(config, sharedCode, securityConfig);
111
+ default: return this._generateCloudflare(config, sharedCode, securityConfig);
112
+ }
113
+ }
114
+
115
+ // ════════════════════════════════════════════════════════════
116
+ // CORS, Health Check, Error Handler helpers
117
+ // ════════════════════════════════════════════════════════════
118
+
119
+ /**
120
+ * Emit CORS helper function. Two modes:
121
+ * - With explicit config: origin-checking __getCorsHeaders(req)
122
+ * - Without config (empty cors {}): wildcard __getCorsHeaders()
123
+ */
124
+ _emitEdgeCors(lines, corsConfig) {
125
+ if (!corsConfig) return;
126
+
127
+ lines.push('// ── CORS ──');
128
+
129
+ // Check if config has any meaningful keys
130
+ const hasOrigins = corsConfig.origins;
131
+ const hasCredentials = corsConfig.credentials;
132
+ const hasMethods = corsConfig.methods;
133
+ const hasHeaders = corsConfig.headers;
134
+ const hasMaxAge = corsConfig.max_age;
135
+ const hasExplicitConfig = hasOrigins || hasCredentials || hasMethods || hasHeaders || hasMaxAge;
136
+
137
+ if (hasExplicitConfig) {
138
+ const origins = hasOrigins ? this.genExpression(corsConfig.origins) : '["*"]';
139
+ const methods = hasMethods ? this.genExpression(corsConfig.methods) + '.join(", ")' : '"GET, POST, PUT, DELETE, PATCH, OPTIONS"';
140
+ const headers = hasHeaders ? this.genExpression(corsConfig.headers) + '.join(", ")' : '"Content-Type, Authorization"';
141
+ const credentials = hasCredentials ? this.genExpression(corsConfig.credentials) : 'false';
142
+ const maxAge = hasMaxAge ? 'String(' + this.genExpression(corsConfig.max_age) + ')' : '"86400"';
143
+
144
+ lines.push(`const __corsOrigins = ${origins};`);
145
+ lines.push('function __getCorsHeaders(req) {');
146
+ lines.push(' const origin = (req && req.headers && req.headers.get) ? req.headers.get("Origin") : "*";');
147
+ lines.push(' const allowed = __corsOrigins.includes("*") || __corsOrigins.includes(origin);');
148
+ lines.push(' return {');
149
+ lines.push(` "Access-Control-Allow-Origin": allowed ? origin : "",`);
150
+ lines.push(` "Access-Control-Allow-Methods": ${methods},`);
151
+ lines.push(` "Access-Control-Allow-Headers": ${headers},`);
152
+ lines.push(` "Access-Control-Allow-Credentials": String(${credentials}),`);
153
+ lines.push(` "Access-Control-Max-Age": ${maxAge},`);
154
+ lines.push(' };');
155
+ lines.push('}');
156
+ } else {
157
+ // Empty cors {} — open wildcard
158
+ lines.push('const __corsHeaders = {');
159
+ lines.push(' "Access-Control-Allow-Origin": "*",');
160
+ lines.push(' "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",');
161
+ lines.push(' "Access-Control-Allow-Headers": "Content-Type, Authorization",');
162
+ lines.push(' "Access-Control-Max-Age": "86400",');
163
+ lines.push('};');
164
+ lines.push('function __getCorsHeaders() { return __corsHeaders; }');
165
+ }
166
+ lines.push('');
167
+ }
168
+
169
+ /**
170
+ * Emit health check route registration.
171
+ * @param {string[]} lines — output lines
172
+ * @param {Object} config — merged edge config (needs healthPath, healthChecks)
173
+ * @param {string} format — 'response' or 'lambda'
174
+ */
175
+ _emitEdgeHealthCheck(lines, config, format) {
176
+ if (!config.healthPath) return;
177
+
178
+ lines.push('// ── Health Check ──');
179
+ const path = config.healthPath;
180
+ const regexStr = '^' + path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$';
181
+
182
+ lines.push(`__routes.push({ method: "GET", pattern: new RegExp(${JSON.stringify(regexStr)}), paramNames: [], handler: async () => {`);
183
+
184
+ if (config.healthChecks && config.healthChecks.length > 0) {
185
+ lines.push(' const checks = {};');
186
+ lines.push(' let status = "healthy";');
187
+ if (config.healthChecks.includes('check_memory')) {
188
+ lines.push(' const mem = process.memoryUsage ? process.memoryUsage() : { heapUsed: 0, heapTotal: 1 };');
189
+ lines.push(' const heapPct = mem.heapUsed / mem.heapTotal;');
190
+ lines.push(' checks.memory = { status: heapPct > 0.9 ? "degraded" : "healthy", heapUsed: mem.heapUsed, heapTotal: mem.heapTotal };');
191
+ lines.push(' if (heapPct > 0.9) status = "degraded";');
192
+ }
193
+ if (format === 'lambda') {
194
+ lines.push(' return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status, checks, timestamp: new Date().toISOString() }) };');
195
+ } else {
196
+ lines.push(' return Response.json({ status, checks, timestamp: new Date().toISOString() });');
197
+ }
198
+ } else {
199
+ if (format === 'lambda') {
200
+ lines.push(' return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "ok" }) };');
201
+ } else {
202
+ lines.push(' return Response.json({ status: "ok" });');
203
+ }
204
+ }
205
+
206
+ lines.push('}});');
207
+ lines.push('');
208
+ }
209
+
210
+ /**
211
+ * Emit error handler function from ErrorHandlerDeclaration.
212
+ */
213
+ _emitEdgeErrorHandler(lines, errorHandler) {
214
+ if (!errorHandler) return;
215
+
216
+ const params = errorHandler.params.map(p => p.name || this.genExpression(p)).join(', ');
217
+ this.pushScope();
218
+ for (const p of errorHandler.params) this.declareVar(p.name);
219
+ const body = this.genBlockBody(errorHandler.body);
220
+ this.popScope();
221
+
222
+ lines.push('// ── Error Handler ──');
223
+ lines.push(`async function __errorHandler(${params}) {`);
224
+ lines.push(body);
225
+ lines.push('}');
226
+ lines.push('');
227
+ }
228
+
229
+ /**
230
+ * Generate catch block with optional error handler.
231
+ * @param {string[]} lines — output lines
232
+ * @param {boolean} hasErrorHandler — whether __errorHandler is defined
233
+ * @param {boolean} hasCors — whether CORS headers should be merged
234
+ * @param {string} format — 'response' or 'lambda'
235
+ * @param {string} indent — indentation prefix
236
+ * @param {string} reqVar — name of the request variable
237
+ */
238
+ _emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, format, indent, reqVar) {
239
+ lines.push(`${indent}} catch (e) {`);
240
+
241
+ if (hasErrorHandler) {
242
+ lines.push(`${indent} if (typeof __errorHandler === "function") {`);
243
+ lines.push(`${indent} try {`);
244
+ lines.push(`${indent} const __errResult = await __errorHandler(e, ${reqVar});`);
245
+
246
+ if (format === 'lambda') {
247
+ lines.push(`${indent} if (__errResult && __errResult.statusCode) return __errResult;`);
248
+ const lambdaHeaders = hasCors
249
+ ? `{ "Content-Type": "application/json", ...__getCorsHeaders(${reqVar}) }`
250
+ : '{ "Content-Type": "application/json" }';
251
+ lines.push(`${indent} return { statusCode: 500, headers: ${lambdaHeaders}, body: JSON.stringify(__errResult) };`);
252
+ } else {
253
+ lines.push(`${indent} if (__errResult instanceof Response) return __errResult;`);
254
+ const respHeaders = hasCors
255
+ ? `{ "Content-Type": "application/json", ...__getCorsHeaders(${reqVar}) }`
256
+ : '{ "Content-Type": "application/json" }';
257
+ lines.push(`${indent} return new Response(JSON.stringify(__errResult), { status: 500, headers: ${respHeaders} });`);
258
+ }
259
+ lines.push(`${indent} } catch (_) {}`);
260
+ lines.push(`${indent} }`);
261
+ }
262
+
263
+ if (format === 'lambda') {
264
+ const fallbackHeaders = hasCors
265
+ ? `{ "Content-Type": "application/json", ...__getCorsHeaders(${reqVar}) }`
266
+ : '{ "Content-Type": "application/json" }';
267
+ lines.push(`${indent} return { statusCode: 500, headers: ${fallbackHeaders}, body: JSON.stringify({ error: e.message }) };`);
268
+ } else {
269
+ if (hasCors) {
270
+ lines.push(`${indent} return new Response(JSON.stringify({ error: e.message }), {`);
271
+ lines.push(`${indent} status: 500,`);
272
+ lines.push(`${indent} headers: { "Content-Type": "application/json", ...__getCorsHeaders(${reqVar}) }`);
273
+ lines.push(`${indent} });`);
274
+ } else {
275
+ lines.push(`${indent} return new Response(JSON.stringify({ error: e.message }), {`);
276
+ lines.push(`${indent} status: 500,`);
277
+ lines.push(`${indent} headers: { "Content-Type": "application/json" }`);
278
+ lines.push(`${indent} });`);
279
+ }
280
+ }
281
+ lines.push(`${indent}}`);
282
+ }
283
+
284
+ // ════════════════════════════════════════════════════════════
285
+ // Security helpers
286
+ // ════════════════════════════════════════════════════════════
287
+
288
+ /**
289
+ * Emit security code (roles, auth, protection, sanitization) from security block config.
290
+ * Returns { hasAuth, hasProtect, hasAutoSanitize } flags.
291
+ */
292
+ _emitEdgeSecurity(lines, securityConfig) {
293
+ const noSec = { hasAuth: false, hasProtect: false, hasAutoSanitize: false };
294
+ if (!securityConfig) return noSec;
295
+
296
+ if (!_SecurityCodegen) _SecurityCodegen = _require('./security-codegen.js').SecurityCodegen;
297
+ const secGen = new _SecurityCodegen();
298
+ const fragments = secGen.generateServerSecurity(securityConfig);
299
+
300
+ if (fragments.roleDefinitions) {
301
+ lines.push(fragments.roleDefinitions);
302
+ lines.push('');
303
+ }
304
+ if (fragments.protectCode) {
305
+ lines.push(fragments.protectCode);
306
+ lines.push('');
307
+ }
308
+ if (fragments.sensitiveCode) {
309
+ lines.push(fragments.sensitiveCode);
310
+ lines.push('');
311
+ }
312
+ if (fragments.cspCode) {
313
+ lines.push(fragments.cspCode);
314
+ lines.push('');
315
+ }
316
+ if (fragments.auditCode) {
317
+ lines.push(fragments.auditCode);
318
+ lines.push('');
319
+ }
320
+
321
+ const hasAuth = this._emitEdgeAuth(lines, securityConfig);
322
+
323
+ return {
324
+ hasAuth,
325
+ hasProtect: !!fragments.protectCode,
326
+ hasAutoSanitize: fragments.hasAutoSanitize,
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Emit JWT auth verification function for edge runtimes.
332
+ * Uses Web Crypto API (available on all edge targets).
333
+ */
334
+ _emitEdgeAuth(lines, securityConfig) {
335
+ if (!securityConfig.auth) return false;
336
+
337
+ const authType = securityConfig.auth.authType;
338
+ if (authType !== 'jwt') return false;
339
+
340
+ const secret = securityConfig.auth.config.secret
341
+ ? this.genExpression(securityConfig.auth.config.secret)
342
+ : 'undefined';
343
+
344
+ lines.push('// ── Edge Auth (JWT) ──');
345
+ lines.push(`const __authSecret = ${secret};`);
346
+ lines.push('async function __authenticate(request) {');
347
+ lines.push(' const __authHdr = (request.headers && request.headers.get) ? request.headers.get("authorization") : (request.headers && (request.headers["Authorization"] || request.headers["authorization"]));');
348
+ lines.push(' if (!__authHdr || !__authHdr.startsWith("Bearer ")) return null;');
349
+ lines.push(' const __token = __authHdr.slice(7);');
350
+ lines.push(' try {');
351
+ lines.push(' const [__hB64, __pB64, __sB64] = __token.split(".");');
352
+ lines.push(' if (!__hB64 || !__pB64 || !__sB64) return null;');
353
+ lines.push(' const __b64d = (s) => atob(s.replace(/-/g, "+").replace(/_/g, "/"));');
354
+ lines.push(' const __hdr = JSON.parse(__b64d(__hB64));');
355
+ lines.push(' if (__hdr.alg !== "HS256") return null;');
356
+ lines.push(' const __payload = JSON.parse(__b64d(__pB64));');
357
+ lines.push(' if (__payload.exp && __payload.exp < Date.now() / 1000) return null;');
358
+ lines.push(' const __enc = new TextEncoder();');
359
+ lines.push(' const __key = await crypto.subtle.importKey("raw", __enc.encode(__authSecret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);');
360
+ lines.push(' const __sigBytes = Uint8Array.from(__b64d(__sB64), c => c.charCodeAt(0));');
361
+ lines.push(' const __valid = await crypto.subtle.verify("HMAC", __key, __sigBytes, __enc.encode(__hB64 + "." + __pB64));');
362
+ lines.push(' if (!__valid) return null;');
363
+ lines.push(' return __payload;');
364
+ lines.push(' } catch (_) { return null; }');
365
+ lines.push('}');
366
+ lines.push('');
367
+ return true;
368
+ }
369
+
370
+ /**
371
+ * Emit inline security check (auth + protection) in request handler.
372
+ * @returns {string} The user variable name ('__user' or 'null')
373
+ */
374
+ _emitEdgeSecurityCheck(lines, secFlags, format, indent, reqVar, hasCors) {
375
+ if (!secFlags.hasAuth && !secFlags.hasProtect) return 'null';
376
+
377
+ const userVar = secFlags.hasAuth ? '__user' : 'null';
378
+
379
+ if (secFlags.hasAuth) {
380
+ lines.push(`${indent}const __user = await __authenticate(${reqVar});`);
381
+ }
382
+
383
+ if (secFlags.hasProtect) {
384
+ lines.push(`${indent}const __prot = __checkProtection(pathname, ${userVar});`);
385
+ lines.push(`${indent}if (!__prot.allowed) {`);
386
+ if (format === 'lambda') {
387
+ const hdr = hasCors
388
+ ? `{ "Content-Type": "application/json", ...__getCorsHeaders(${reqVar}) }`
389
+ : '{ "Content-Type": "application/json" }';
390
+ lines.push(`${indent} return { statusCode: ${secFlags.hasAuth ? '(__user ? 403 : 401)' : '403'}, headers: ${hdr}, body: JSON.stringify({ error: __prot.reason }) };`);
391
+ } else {
392
+ const hdr = hasCors
393
+ ? `{ "Content-Type": "application/json", ...__getCorsHeaders(${reqVar}) }`
394
+ : '{ "Content-Type": "application/json" }';
395
+ lines.push(`${indent} return new Response(JSON.stringify({ error: __prot.reason }), { status: ${secFlags.hasAuth ? '(__user ? 403 : 401)' : '403'}, headers: ${hdr} });`);
396
+ }
397
+ lines.push(`${indent}}`);
398
+ lines.push('');
399
+ }
400
+
401
+ return userVar;
402
+ }
403
+
404
+ // ════════════════════════════════════════════════════════════
405
+ // Binding helpers
406
+ // ════════════════════════════════════════════════════════════
407
+
408
+ /**
409
+ * Generate the ` ?? defaultExpr` suffix for an env declaration.
410
+ */
411
+ _genDefaultSuffix(envDecl) {
412
+ if (envDecl.defaultValue) {
413
+ return ' ?? ' + this.genExpression(envDecl.defaultValue);
414
+ }
415
+ return '';
416
+ }
417
+
418
+ /**
419
+ * Cloudflare bindings: module-level `let` declarations + init lines for fetch/scheduled/queue.
420
+ * Returns { moduleLines: string[], fetchInitLines: string[] }
421
+ */
422
+ _emitCloudflareBindings(config) {
423
+ const moduleLines = [];
424
+ const fetchInitLines = [];
425
+ const allBindings = [
426
+ ...config.bindings.kv,
427
+ ...config.bindings.sql,
428
+ ...config.bindings.storage,
429
+ ...config.bindings.queue,
430
+ ];
431
+ const allEnvSecrets = [...config.envVars, ...config.secrets];
432
+
433
+ if (allBindings.length === 0 && allEnvSecrets.length === 0) {
434
+ return { moduleLines, fetchInitLines };
435
+ }
436
+
437
+ // Module-level let declarations
438
+ const names = [
439
+ ...allBindings.map(b => b.name),
440
+ ...allEnvSecrets.map(b => b.name),
441
+ ];
442
+ moduleLines.push('// ── Bindings ──');
443
+ moduleLines.push('let ' + names.join(', ') + ';');
444
+ moduleLines.push('');
445
+
446
+ // Fetch init lines (inside fetch/scheduled/queue handlers)
447
+ for (const b of allBindings) {
448
+ fetchInitLines.push(` ${b.name} = env.${b.name};`);
449
+ }
450
+ for (const e of config.envVars) {
451
+ fetchInitLines.push(` ${e.name} = env.${e.name}${this._genDefaultSuffix(e)};`);
452
+ }
453
+ for (const s of config.secrets) {
454
+ fetchInitLines.push(` ${s.name} = env.${s.name};`);
455
+ }
456
+
457
+ return { moduleLines, fetchInitLines };
458
+ }
459
+
460
+ /**
461
+ * Deno bindings: top-level const declarations.
462
+ */
463
+ _emitDenoBindings(lines, config) {
464
+ const hasKv = config.bindings.kv.length > 0;
465
+ const hasAnything = hasKv || config.bindings.sql.length > 0 ||
466
+ config.bindings.storage.length > 0 || config.bindings.queue.length > 0 ||
467
+ config.envVars.length > 0 || config.secrets.length > 0;
468
+
469
+ if (!hasAnything) return;
470
+
471
+ lines.push('// ── Bindings ──');
472
+
473
+ // KV — first one opens the store, rest share
474
+ if (hasKv) {
475
+ lines.push(`const ${config.bindings.kv[0].name} = await Deno.openKv();`);
476
+ for (let i = 1; i < config.bindings.kv.length; i++) {
477
+ lines.push(`const ${config.bindings.kv[i].name} = ${config.bindings.kv[0].name}; // shared Deno KV store`);
478
+ }
479
+ }
480
+
481
+ // Unsupported stubs
482
+ for (const b of config.bindings.sql) {
483
+ lines.push(`const ${b.name} = null; // SQL not natively supported on Deno Deploy — use a third-party driver`);
484
+ }
485
+ for (const b of config.bindings.storage) {
486
+ lines.push(`const ${b.name} = null; // Object storage not natively supported on Deno Deploy`);
487
+ }
488
+ for (const b of config.bindings.queue) {
489
+ lines.push(`const ${b.name} = null; // Queues not natively supported on Deno Deploy`);
490
+ }
491
+
492
+ // Env/Secret
493
+ for (const e of config.envVars) {
494
+ lines.push(`const ${e.name} = Deno.env.get(${JSON.stringify(e.name)})${this._genDefaultSuffix(e)};`);
495
+ }
496
+ for (const s of config.secrets) {
497
+ lines.push(`const ${s.name} = Deno.env.get(${JSON.stringify(s.name)});`);
498
+ }
499
+
500
+ lines.push('');
501
+ }
502
+
503
+ /**
504
+ * Process.env-based bindings (Vercel, Lambda).
505
+ * Only env/secret are supported; others become stubs.
506
+ */
507
+ _emitProcessEnvBindings(lines, config, targetName) {
508
+ const hasAnything = config.bindings.kv.length > 0 || config.bindings.sql.length > 0 ||
509
+ config.bindings.storage.length > 0 || config.bindings.queue.length > 0 ||
510
+ config.envVars.length > 0 || config.secrets.length > 0;
511
+
512
+ if (!hasAnything) return;
513
+
514
+ lines.push('// ── Bindings ──');
515
+
516
+ // Unsupported stubs
517
+ for (const b of config.bindings.kv) {
518
+ lines.push(`const ${b.name} = null; // KV not supported on ${targetName}`);
519
+ }
520
+ for (const b of config.bindings.sql) {
521
+ lines.push(`const ${b.name} = null; // SQL not supported on ${targetName}`);
522
+ }
523
+ for (const b of config.bindings.storage) {
524
+ lines.push(`const ${b.name} = null; // Object storage not supported on ${targetName}`);
525
+ }
526
+ for (const b of config.bindings.queue) {
527
+ lines.push(`const ${b.name} = null; // Queues not supported on ${targetName}`);
528
+ }
529
+
530
+ // Env/Secret via process.env
531
+ for (const e of config.envVars) {
532
+ lines.push(`const ${e.name} = process.env.${e.name}${this._genDefaultSuffix(e)};`);
533
+ }
534
+ for (const s of config.secrets) {
535
+ lines.push(`const ${s.name} = process.env.${s.name};`);
536
+ }
537
+
538
+ lines.push('');
539
+ }
540
+
541
+ /**
542
+ * Bun bindings: SQL via bun:sqlite, env via process.env, others stub.
543
+ * Returns { imports: string[], bindings: string[] }
544
+ */
545
+ _emitBunBindings(config) {
546
+ const imports = [];
547
+ const bindings = [];
548
+ const hasAnything = config.bindings.kv.length > 0 || config.bindings.sql.length > 0 ||
549
+ config.bindings.storage.length > 0 || config.bindings.queue.length > 0 ||
550
+ config.envVars.length > 0 || config.secrets.length > 0;
551
+
552
+ if (!hasAnything) return { imports, bindings };
553
+
554
+ bindings.push('// ── Bindings ──');
555
+
556
+ // KV stub
557
+ for (const b of config.bindings.kv) {
558
+ bindings.push(`const ${b.name} = null; // KV not natively supported on Bun — use a third-party store`);
559
+ }
560
+
561
+ // SQL via bun:sqlite
562
+ if (config.bindings.sql.length > 0) {
563
+ imports.push('import { Database } from "bun:sqlite";');
564
+ for (const b of config.bindings.sql) {
565
+ bindings.push(`const ${b.name} = new Database("${b.name}.sqlite");`);
566
+ }
567
+ }
568
+
569
+ // Storage/Queue stubs
570
+ for (const b of config.bindings.storage) {
571
+ bindings.push(`const ${b.name} = null; // Object storage not natively supported on Bun`);
572
+ }
573
+ for (const b of config.bindings.queue) {
574
+ bindings.push(`const ${b.name} = null; // Queues not natively supported on Bun`);
575
+ }
576
+
577
+ // Env/Secret via process.env
578
+ for (const e of config.envVars) {
579
+ bindings.push(`const ${e.name} = process.env.${e.name}${this._genDefaultSuffix(e)};`);
580
+ }
581
+ for (const s of config.secrets) {
582
+ bindings.push(`const ${s.name} = process.env.${s.name};`);
583
+ }
584
+
585
+ bindings.push('');
586
+ return { imports, bindings };
587
+ }
588
+
589
+ // ════════════════════════════════════════════════════════════
590
+ // Cloudflare Workers target
591
+ // ════════════════════════════════════════════════════════════
592
+
593
+ _generateCloudflare(config, sharedCode, securityConfig) {
594
+ const lines = [];
595
+ const hasCors = !!config.corsConfig;
596
+ const hasErrorHandler = !!config.errorHandler;
597
+
598
+ lines.push('// Generated by Tova — Cloudflare Workers target');
599
+ lines.push('');
600
+
601
+ // Shared code
602
+ if (sharedCode && sharedCode.trim()) {
603
+ lines.push(sharedCode);
604
+ lines.push('');
605
+ }
606
+
607
+ // Binding declarations (module-level let + fetch init lines)
608
+ const { moduleLines, fetchInitLines } = this._emitCloudflareBindings(config);
609
+ for (const l of moduleLines) lines.push(l);
610
+
611
+ // User functions
612
+ this._emitFunctions(lines, config.functions);
613
+
614
+ // Misc statements (assignments, etc.)
615
+ this._emitMiscStatements(lines, config.miscStatements);
616
+
617
+ // CORS
618
+ this._emitEdgeCors(lines, config.corsConfig);
619
+
620
+ // Error handler
621
+ this._emitEdgeErrorHandler(lines, config.errorHandler);
622
+
623
+ // Security (roles, auth, protection, sanitization)
624
+ const secFlags = this._emitEdgeSecurity(lines, securityConfig);
625
+
626
+ // Middleware chain
627
+ this._emitMiddlewareFunctions(lines, config.middlewares);
628
+
629
+ // Route matching helper
630
+ this._emitRouteMatchHelper(lines);
631
+
632
+ // Build route table
633
+ lines.push('// ── Route Table ──');
634
+ lines.push('const __routes = [];');
635
+ this._emitRouteRegistrations(lines, config.routes);
636
+
637
+ // Health check route
638
+ this._emitEdgeHealthCheck(lines, config, 'response');
639
+ lines.push('');
640
+
641
+ // Fetch handler
642
+ lines.push('export default {');
643
+ lines.push(' async fetch(request, env, ctx) {');
644
+
645
+ // Init bindings from env
646
+ for (const l of fetchInitLines) lines.push(l);
647
+
648
+ lines.push(' const url = new URL(request.url);');
649
+ lines.push(' const method = request.method;');
650
+ lines.push(' const pathname = url.pathname;');
651
+ lines.push('');
652
+
653
+ // OPTIONS preflight
654
+ if (hasCors) {
655
+ lines.push(' if (request.method === "OPTIONS") {');
656
+ lines.push(' return new Response(null, { status: 204, headers: __getCorsHeaders(request) });');
657
+ lines.push(' }');
658
+ lines.push('');
659
+ }
660
+
661
+ // Security check (auth + protection)
662
+ const userVar = this._emitEdgeSecurityCheck(lines, secFlags, 'response', ' ', 'request', hasCors);
663
+ const sanitize = secFlags.hasAutoSanitize;
664
+
665
+ // Middleware wrapping
666
+ if (config.middlewares.length > 0) {
667
+ lines.push(' // Apply middleware chain');
668
+ lines.push(' const __handler = async (req) => {');
669
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
670
+ lines.push(' if (!__match) return new Response("Not Found", { status: 404 });');
671
+ lines.push(' return __match.handler(req, __match.params, env);');
672
+ lines.push(' };');
673
+ let chain = '__handler';
674
+ for (let i = config.middlewares.length - 1; i >= 0; i--) {
675
+ const mw = config.middlewares[i];
676
+ chain = `(req) => __mw_${mw.name}(req, ${chain})`;
677
+ }
678
+ lines.push(' try {');
679
+ lines.push(` const __result = await (${chain})(request);`);
680
+ lines.push(' if (__result instanceof Response) return __result;');
681
+ const mwVal = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
682
+ if (hasCors) {
683
+ lines.push(` return new Response(JSON.stringify(${mwVal}), { headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) } });`);
684
+ } else {
685
+ lines.push(` return Response.json(${mwVal});`);
686
+ }
687
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'response', ' ', 'request');
688
+ } else {
689
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
690
+ lines.push(' if (!__match) return new Response("Not Found", { status: 404 });');
691
+ lines.push('');
692
+ lines.push(' try {');
693
+ lines.push(' const __result = await __match.handler(request, __match.params, env);');
694
+ lines.push(' if (__result instanceof Response) return __result;');
695
+ const val = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
696
+ if (hasCors) {
697
+ lines.push(` return new Response(JSON.stringify(${val}), { headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) } });`);
698
+ } else {
699
+ lines.push(` return Response.json(${val});`);
700
+ }
701
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'response', ' ', 'request');
702
+ }
703
+ lines.push(' },');
704
+
705
+ // Scheduled handler
706
+ if (config.schedules.length > 0) {
707
+ lines.push('');
708
+ lines.push(' async scheduled(event, env, ctx) {');
709
+ // Init bindings in scheduled handler too
710
+ for (const l of fetchInitLines) lines.push(l);
711
+ for (let si = 0; si < config.schedules.length; si++) {
712
+ const sched = config.schedules[si];
713
+ const kw = si === 0 ? 'if' : 'else if';
714
+ lines.push(` ${kw} (event.cron === ${JSON.stringify(sched.cron)}) {`);
715
+ lines.push(` // ${sched.name}`);
716
+ const body = this.genBlockStatements(sched.body);
717
+ for (const line of body.split('\n')) {
718
+ lines.push(' ' + line);
719
+ }
720
+ lines.push(' }');
721
+ }
722
+ lines.push(' },');
723
+ }
724
+
725
+ // Queue consumer
726
+ if (config.consumers.length > 0) {
727
+ lines.push('');
728
+ lines.push(' async queue(batch, env, ctx) {');
729
+ // Init bindings in queue handler too
730
+ for (const l of fetchInitLines) lines.push(l);
731
+ for (const consumer of config.consumers) {
732
+ lines.push(` // consume ${consumer.queue}`);
733
+ const handlerCode = this.genExpression(consumer.handler);
734
+ lines.push(` await (${handlerCode})(batch.messages);`);
735
+ }
736
+ lines.push(' },');
737
+ }
738
+
739
+ lines.push('};');
740
+
741
+ return lines.join('\n');
742
+ }
743
+
744
+ // ════════════════════════════════════════════════════════════
745
+ // Deno Deploy target
746
+ // ════════════════════════════════════════════════════════════
747
+
748
+ _generateDeno(config, sharedCode, securityConfig) {
749
+ const lines = [];
750
+ const hasCors = !!config.corsConfig;
751
+ const hasErrorHandler = !!config.errorHandler;
752
+
753
+ lines.push('// Generated by Tova — Deno Deploy target');
754
+ lines.push('');
755
+
756
+ // Shared code
757
+ if (sharedCode && sharedCode.trim()) {
758
+ lines.push(sharedCode);
759
+ lines.push('');
760
+ }
761
+
762
+ // Bindings
763
+ this._emitDenoBindings(lines, config);
764
+
765
+ // User functions
766
+ this._emitFunctions(lines, config.functions);
767
+
768
+ // Misc statements
769
+ this._emitMiscStatements(lines, config.miscStatements);
770
+
771
+ // CORS
772
+ this._emitEdgeCors(lines, config.corsConfig);
773
+
774
+ // Error handler
775
+ this._emitEdgeErrorHandler(lines, config.errorHandler);
776
+
777
+ // Security
778
+ const secFlags = this._emitEdgeSecurity(lines, securityConfig);
779
+
780
+ // Middleware
781
+ this._emitMiddlewareFunctions(lines, config.middlewares);
782
+
783
+ // Route matching helper
784
+ this._emitRouteMatchHelper(lines);
785
+
786
+ // Build route table
787
+ lines.push('// ── Route Table ──');
788
+ lines.push('const __routes = [];');
789
+ this._emitRouteRegistrations(lines, config.routes);
790
+
791
+ // Health check route
792
+ this._emitEdgeHealthCheck(lines, config, 'response');
793
+ lines.push('');
794
+
795
+ // Cron schedules
796
+ for (const sched of config.schedules) {
797
+ lines.push(`Deno.cron(${JSON.stringify(sched.name)}, ${JSON.stringify(sched.cron)}, async () => {`);
798
+ const body = this.genBlockStatements(sched.body);
799
+ for (const line of body.split('\n')) {
800
+ lines.push(' ' + line);
801
+ }
802
+ lines.push('});');
803
+ lines.push('');
804
+ }
805
+
806
+ // Server
807
+ lines.push('Deno.serve(async (request) => {');
808
+ lines.push(' const url = new URL(request.url);');
809
+ lines.push(' const method = request.method;');
810
+ lines.push(' const pathname = url.pathname;');
811
+ lines.push('');
812
+
813
+ // OPTIONS preflight
814
+ if (hasCors) {
815
+ lines.push(' if (request.method === "OPTIONS") {');
816
+ lines.push(' return new Response(null, { status: 204, headers: __getCorsHeaders(request) });');
817
+ lines.push(' }');
818
+ lines.push('');
819
+ }
820
+
821
+ // Security check
822
+ const userVar = this._emitEdgeSecurityCheck(lines, secFlags, 'response', ' ', 'request', hasCors);
823
+ const sanitize = secFlags.hasAutoSanitize;
824
+
825
+ if (config.middlewares.length > 0) {
826
+ lines.push(' const __handler = async (req) => {');
827
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
828
+ lines.push(' if (!__match) return new Response("Not Found", { status: 404 });');
829
+ lines.push(' return __match.handler(req, __match.params);');
830
+ lines.push(' };');
831
+ let chain = '__handler';
832
+ for (let i = config.middlewares.length - 1; i >= 0; i--) {
833
+ const mw = config.middlewares[i];
834
+ chain = `(req) => __mw_${mw.name}(req, ${chain})`;
835
+ }
836
+ lines.push(' try {');
837
+ lines.push(` const __result = await (${chain})(request);`);
838
+ lines.push(' if (__result instanceof Response) return __result;');
839
+ const mwVal = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
840
+ if (hasCors) {
841
+ lines.push(` return new Response(JSON.stringify(${mwVal}), { headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) } });`);
842
+ } else {
843
+ lines.push(` return Response.json(${mwVal});`);
844
+ }
845
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'response', ' ', 'request');
846
+ } else {
847
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
848
+ lines.push(' if (!__match) return new Response("Not Found", { status: 404 });');
849
+ lines.push('');
850
+ lines.push(' try {');
851
+ lines.push(' const __result = await __match.handler(request, __match.params);');
852
+ lines.push(' if (__result instanceof Response) return __result;');
853
+ const val = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
854
+ if (hasCors) {
855
+ lines.push(` return new Response(JSON.stringify(${val}), { headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) } });`);
856
+ } else {
857
+ lines.push(` return Response.json(${val});`);
858
+ }
859
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'response', ' ', 'request');
860
+ }
861
+ lines.push('});');
862
+
863
+ return lines.join('\n');
864
+ }
865
+
866
+ // ════════════════════════════════════════════════════════════
867
+ // Vercel Edge target
868
+ // ════════════════════════════════════════════════════════════
869
+
870
+ _generateVercel(config, sharedCode, securityConfig) {
871
+ const lines = [];
872
+ const hasCors = !!config.corsConfig;
873
+ const hasErrorHandler = !!config.errorHandler;
874
+
875
+ lines.push('// Generated by Tova — Vercel Edge target');
876
+ lines.push('');
877
+ lines.push('export const config = { runtime: "edge" };');
878
+ lines.push('');
879
+
880
+ if (sharedCode && sharedCode.trim()) {
881
+ lines.push(sharedCode);
882
+ lines.push('');
883
+ }
884
+
885
+ // Bindings
886
+ this._emitProcessEnvBindings(lines, config, 'Vercel Edge');
887
+
888
+ this._emitFunctions(lines, config.functions);
889
+ this._emitMiscStatements(lines, config.miscStatements);
890
+
891
+ // CORS
892
+ this._emitEdgeCors(lines, config.corsConfig);
893
+
894
+ // Error handler
895
+ this._emitEdgeErrorHandler(lines, config.errorHandler);
896
+
897
+ // Security
898
+ const secFlags = this._emitEdgeSecurity(lines, securityConfig);
899
+
900
+ this._emitMiddlewareFunctions(lines, config.middlewares);
901
+
902
+ this._emitRouteMatchHelper(lines);
903
+
904
+ lines.push('const __routes = [];');
905
+ this._emitRouteRegistrations(lines, config.routes);
906
+
907
+ // Health check route
908
+ this._emitEdgeHealthCheck(lines, config, 'response');
909
+ lines.push('');
910
+
911
+ lines.push('export default async function handler(request) {');
912
+ lines.push(' const url = new URL(request.url);');
913
+ lines.push(' const method = request.method;');
914
+ lines.push(' const pathname = url.pathname;');
915
+
916
+ // OPTIONS preflight
917
+ if (hasCors) {
918
+ lines.push(' if (request.method === "OPTIONS") {');
919
+ lines.push(' return new Response(null, { status: 204, headers: __getCorsHeaders(request) });');
920
+ lines.push(' }');
921
+ }
922
+
923
+ // Security check
924
+ const userVar = this._emitEdgeSecurityCheck(lines, secFlags, 'response', ' ', 'request', hasCors);
925
+ const sanitize = secFlags.hasAutoSanitize;
926
+
927
+ if (config.middlewares.length > 0) {
928
+ lines.push(' // Apply middleware chain');
929
+ lines.push(' const __handler = async (req) => {');
930
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
931
+ lines.push(' if (!__match) return new Response("Not Found", { status: 404 });');
932
+ lines.push(' return __match.handler(req, __match.params);');
933
+ lines.push(' };');
934
+ let chain = '__handler';
935
+ for (let i = config.middlewares.length - 1; i >= 0; i--) {
936
+ const mw = config.middlewares[i];
937
+ chain = `(req) => __mw_${mw.name}(req, ${chain})`;
938
+ }
939
+ lines.push(' try {');
940
+ lines.push(` const __result = await (${chain})(request);`);
941
+ lines.push(' if (__result instanceof Response) return __result;');
942
+ const mwVal = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
943
+ if (hasCors) {
944
+ lines.push(` return new Response(JSON.stringify(${mwVal}), { headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) } });`);
945
+ } else {
946
+ lines.push(` return Response.json(${mwVal});`);
947
+ }
948
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'response', ' ', 'request');
949
+ } else {
950
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
951
+ lines.push(' if (!__match) return new Response("Not Found", { status: 404 });');
952
+ lines.push(' try {');
953
+ lines.push(' const __result = await __match.handler(request, __match.params);');
954
+ lines.push(' if (__result instanceof Response) return __result;');
955
+ const val = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
956
+ if (hasCors) {
957
+ lines.push(` return new Response(JSON.stringify(${val}), { headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) } });`);
958
+ } else {
959
+ lines.push(` return Response.json(${val});`);
960
+ }
961
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'response', ' ', 'request');
962
+ }
963
+ lines.push('}');
964
+
965
+ return lines.join('\n');
966
+ }
967
+
968
+ // ════════════════════════════════════════════════════════════
969
+ // AWS Lambda target
970
+ // ════════════════════════════════════════════════════════════
971
+
972
+ _generateLambda(config, sharedCode, securityConfig) {
973
+ const lines = [];
974
+ const hasCors = !!config.corsConfig;
975
+ const hasErrorHandler = !!config.errorHandler;
976
+
977
+ lines.push('// Generated by Tova — AWS Lambda target');
978
+ lines.push('');
979
+
980
+ if (sharedCode && sharedCode.trim()) {
981
+ lines.push(sharedCode);
982
+ lines.push('');
983
+ }
984
+
985
+ // Bindings
986
+ this._emitProcessEnvBindings(lines, config, 'AWS Lambda');
987
+
988
+ this._emitFunctions(lines, config.functions);
989
+ this._emitMiscStatements(lines, config.miscStatements);
990
+
991
+ // CORS
992
+ this._emitEdgeCors(lines, config.corsConfig);
993
+
994
+ // Error handler
995
+ this._emitEdgeErrorHandler(lines, config.errorHandler);
996
+
997
+ // Security
998
+ const secFlags = this._emitEdgeSecurity(lines, securityConfig);
999
+
1000
+ this._emitMiddlewareFunctions(lines, config.middlewares);
1001
+
1002
+ this._emitRouteMatchHelper(lines);
1003
+
1004
+ lines.push('const __routes = [];');
1005
+ this._emitRouteRegistrations(lines, config.routes);
1006
+
1007
+ // Health check route
1008
+ this._emitEdgeHealthCheck(lines, config, 'lambda');
1009
+ lines.push('');
1010
+
1011
+ // Lambda handler — translate API Gateway event to Request-like object
1012
+ lines.push('export const handler = async (event, context) => {');
1013
+ lines.push(' const method = event.httpMethod || (event.requestContext && event.requestContext.http && event.requestContext.http.method) || "GET";');
1014
+ lines.push(' const pathname = event.path || event.rawPath || "/";');
1015
+ lines.push(' const __rawHeaders = event.headers || {};');
1016
+ lines.push(' const headers = { ...__rawHeaders, get: (k) => __rawHeaders[k] || __rawHeaders[k.toLowerCase()] || __rawHeaders[k.charAt(0).toUpperCase() + k.slice(1).toLowerCase()] || null };');
1017
+ lines.push(' const body = event.body ? (event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body) : null;');
1018
+ lines.push(' const request = { method, path: pathname, headers, body, json: () => JSON.parse(body || "{}"), url: "https://lambda.local" + pathname };');
1019
+ lines.push('');
1020
+
1021
+ // OPTIONS preflight
1022
+ if (hasCors) {
1023
+ lines.push(' if (method === "OPTIONS") {');
1024
+ lines.push(' return { statusCode: 204, headers: __getCorsHeaders(request) };');
1025
+ lines.push(' }');
1026
+ lines.push('');
1027
+ }
1028
+
1029
+ // Security check
1030
+ const userVar = this._emitEdgeSecurityCheck(lines, secFlags, 'lambda', ' ', 'request', hasCors);
1031
+ const sanitize = secFlags.hasAutoSanitize;
1032
+
1033
+ if (config.middlewares.length > 0) {
1034
+ lines.push(' // Apply middleware chain');
1035
+ lines.push(' const __handler = async (req) => {');
1036
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
1037
+ lines.push(' if (!__match) return { statusCode: 404, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error: "Not Found" }) };');
1038
+ lines.push(' const __r = await __match.handler(req, __match.params);');
1039
+ lines.push(' if (__r && __r.statusCode) return __r;');
1040
+ const mwValInner = sanitize ? `__autoSanitize(__r, ${userVar})` : '__r';
1041
+ if (hasCors) {
1042
+ lines.push(` return { statusCode: 200, headers: { "Content-Type": "application/json", ...__getCorsHeaders(req) }, body: JSON.stringify(${mwValInner}) };`);
1043
+ } else {
1044
+ lines.push(` return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify(${mwValInner}) };`);
1045
+ }
1046
+ lines.push(' };');
1047
+ let chain = '__handler';
1048
+ for (let i = config.middlewares.length - 1; i >= 0; i--) {
1049
+ const mw = config.middlewares[i];
1050
+ chain = `(req) => __mw_${mw.name}(req, ${chain})`;
1051
+ }
1052
+ lines.push(' try {');
1053
+ lines.push(` const __result = await (${chain})(request);`);
1054
+ lines.push(' if (__result && __result.statusCode) return __result;');
1055
+ const mwVal = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
1056
+ if (hasCors) {
1057
+ lines.push(` return { statusCode: 200, headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) }, body: JSON.stringify(${mwVal}) };`);
1058
+ } else {
1059
+ lines.push(` return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify(${mwVal}) };`);
1060
+ }
1061
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'lambda', ' ', 'request');
1062
+ } else {
1063
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
1064
+ lines.push(' if (!__match) return { statusCode: 404, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error: "Not Found" }) };');
1065
+ lines.push('');
1066
+ lines.push(' try {');
1067
+ lines.push(' const __result = await __match.handler(request, __match.params);');
1068
+ lines.push(' if (__result && __result.statusCode) return __result;');
1069
+ const val = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
1070
+ if (hasCors) {
1071
+ lines.push(` return { statusCode: 200, headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) }, body: JSON.stringify(${val}) };`);
1072
+ } else {
1073
+ lines.push(` return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify(${val}) };`);
1074
+ }
1075
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'lambda', ' ', 'request');
1076
+ }
1077
+ lines.push('};');
1078
+
1079
+ return lines.join('\n');
1080
+ }
1081
+
1082
+ // ════════════════════════════════════════════════════════════
1083
+ // Bun target (similar to existing server but edge-optimized)
1084
+ // ════════════════════════════════════════════════════════════
1085
+
1086
+ _generateBun(config, sharedCode, securityConfig) {
1087
+ const lines = [];
1088
+ const hasCors = !!config.corsConfig;
1089
+ const hasErrorHandler = !!config.errorHandler;
1090
+
1091
+ lines.push('// Generated by Tova — Bun edge target');
1092
+ lines.push('');
1093
+
1094
+ // Bun bindings (imports go first)
1095
+ const { imports: bunImports, bindings: bunBindings } = this._emitBunBindings(config);
1096
+ for (const imp of bunImports) lines.push(imp);
1097
+ if (bunImports.length > 0) lines.push('');
1098
+
1099
+ if (sharedCode && sharedCode.trim()) {
1100
+ lines.push(sharedCode);
1101
+ lines.push('');
1102
+ }
1103
+
1104
+ // Binding declarations
1105
+ for (const l of bunBindings) lines.push(l);
1106
+
1107
+ this._emitFunctions(lines, config.functions);
1108
+ this._emitMiscStatements(lines, config.miscStatements);
1109
+
1110
+ // CORS
1111
+ this._emitEdgeCors(lines, config.corsConfig);
1112
+
1113
+ // Error handler
1114
+ this._emitEdgeErrorHandler(lines, config.errorHandler);
1115
+
1116
+ // Security
1117
+ const secFlags = this._emitEdgeSecurity(lines, securityConfig);
1118
+
1119
+ this._emitMiddlewareFunctions(lines, config.middlewares);
1120
+
1121
+ this._emitRouteMatchHelper(lines);
1122
+
1123
+ lines.push('const __routes = [];');
1124
+ this._emitRouteRegistrations(lines, config.routes);
1125
+
1126
+ // Health check route
1127
+ this._emitEdgeHealthCheck(lines, config, 'response');
1128
+ lines.push('');
1129
+
1130
+ lines.push('Bun.serve({');
1131
+ lines.push(' port: process.env.PORT || 3000,');
1132
+ lines.push(' async fetch(request) {');
1133
+ lines.push(' const url = new URL(request.url);');
1134
+ lines.push(' const method = request.method;');
1135
+ lines.push(' const pathname = url.pathname;');
1136
+
1137
+ // OPTIONS preflight
1138
+ if (hasCors) {
1139
+ lines.push(' if (request.method === "OPTIONS") {');
1140
+ lines.push(' return new Response(null, { status: 204, headers: __getCorsHeaders(request) });');
1141
+ lines.push(' }');
1142
+ }
1143
+
1144
+ // Security check
1145
+ const userVar = this._emitEdgeSecurityCheck(lines, secFlags, 'response', ' ', 'request', hasCors);
1146
+ const sanitize = secFlags.hasAutoSanitize;
1147
+
1148
+ if (config.middlewares.length > 0) {
1149
+ lines.push(' const __handler = async (req) => {');
1150
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
1151
+ lines.push(' if (!__match) return new Response("Not Found", { status: 404 });');
1152
+ lines.push(' return __match.handler(req, __match.params);');
1153
+ lines.push(' };');
1154
+ let chain = '__handler';
1155
+ for (let i = config.middlewares.length - 1; i >= 0; i--) {
1156
+ const mw = config.middlewares[i];
1157
+ chain = `(req) => __mw_${mw.name}(req, ${chain})`;
1158
+ }
1159
+ lines.push(' try {');
1160
+ lines.push(` const __result = await (${chain})(request);`);
1161
+ lines.push(' if (__result instanceof Response) return __result;');
1162
+ const mwVal = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
1163
+ if (hasCors) {
1164
+ lines.push(` return new Response(JSON.stringify(${mwVal}), { headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) } });`);
1165
+ } else {
1166
+ lines.push(` return Response.json(${mwVal});`);
1167
+ }
1168
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'response', ' ', 'request');
1169
+ } else {
1170
+ lines.push(' const __match = __matchRoute(method, pathname, __routes);');
1171
+ lines.push(' if (!__match) return new Response("Not Found", { status: 404 });');
1172
+ lines.push(' try {');
1173
+ lines.push(' const __result = await __match.handler(request, __match.params);');
1174
+ lines.push(' if (__result instanceof Response) return __result;');
1175
+ const val = sanitize ? `__autoSanitize(__result, ${userVar})` : '__result';
1176
+ if (hasCors) {
1177
+ lines.push(` return new Response(JSON.stringify(${val}), { headers: { "Content-Type": "application/json", ...__getCorsHeaders(request) } });`);
1178
+ } else {
1179
+ lines.push(` return Response.json(${val});`);
1180
+ }
1181
+ this._emitEdgeCatchBlock(lines, hasErrorHandler, hasCors, 'response', ' ', 'request');
1182
+ }
1183
+ lines.push(' }');
1184
+ lines.push('});');
1185
+
1186
+ return lines.join('\n');
1187
+ }
1188
+
1189
+ // ════════════════════════════════════════════════════════════
1190
+ // Shared helpers
1191
+ // ════════════════════════════════════════════════════════════
1192
+
1193
+ _emitRouteMatchHelper(lines) {
1194
+ lines.push('// ── Route Matching ──');
1195
+ lines.push('function __matchRoute(method, pathname, routes) {');
1196
+ lines.push(' for (const route of routes) {');
1197
+ lines.push(' if (route.method !== method && route.method !== "*") continue;');
1198
+ lines.push(' const match = route.pattern.exec(pathname);');
1199
+ lines.push(' if (match) {');
1200
+ lines.push(' const params = {};');
1201
+ lines.push(' for (let i = 0; i < route.paramNames.length; i++) {');
1202
+ lines.push(' params[route.paramNames[i]] = match[i + 1];');
1203
+ lines.push(' }');
1204
+ lines.push(' return { handler: route.handler, params };');
1205
+ lines.push(' }');
1206
+ lines.push(' }');
1207
+ lines.push(' return null;');
1208
+ lines.push('}');
1209
+ lines.push('');
1210
+ }
1211
+
1212
+ _emitFunctions(lines, functions) {
1213
+ if (functions.length === 0) return;
1214
+ lines.push('// ── Functions ──');
1215
+ for (const fn of functions) {
1216
+ const code = this.generateStatement(fn);
1217
+ lines.push(code);
1218
+ lines.push('');
1219
+ }
1220
+ }
1221
+
1222
+ _emitMiscStatements(lines, stmts) {
1223
+ for (const stmt of stmts) {
1224
+ const code = this.generateStatement(stmt);
1225
+ if (code && code.trim()) {
1226
+ lines.push(code);
1227
+ }
1228
+ }
1229
+ }
1230
+
1231
+ _emitMiddlewareFunctions(lines, middlewares) {
1232
+ if (middlewares.length === 0) return;
1233
+ lines.push('// ── Middleware ──');
1234
+ for (const mw of middlewares) {
1235
+ const params = mw.params.map(p => p.name || this.genExpression(p)).join(', ');
1236
+ const body = this.genBlockStatements(mw.body);
1237
+ lines.push(`async function __mw_${mw.name}(${params}) {`);
1238
+ lines.push(body);
1239
+ lines.push('}');
1240
+ lines.push('');
1241
+ }
1242
+ }
1243
+
1244
+ /**
1245
+ * Convert route path pattern (e.g., "/api/users/:id") to a regex
1246
+ * and emit __routes.push({ method, pattern, paramNames, handler })
1247
+ */
1248
+ _emitRouteRegistrations(lines, routes) {
1249
+ for (const route of routes) {
1250
+ const method = route.method.toUpperCase();
1251
+ const path = route.path;
1252
+
1253
+ // Extract param names and build regex
1254
+ const paramNames = [];
1255
+ const regexParts = path.split('/').map(seg => {
1256
+ if (seg.startsWith(':')) {
1257
+ paramNames.push(seg.slice(1));
1258
+ return '([^/]+)';
1259
+ }
1260
+ if (seg.startsWith('*')) {
1261
+ paramNames.push(seg.slice(1) || 'wild');
1262
+ return '(.*)';
1263
+ }
1264
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1265
+ });
1266
+ const regexStr = '^' + regexParts.join('/') + '$';
1267
+
1268
+ const handler = this.genExpression(route.handler);
1269
+ lines.push(`__routes.push({ method: ${JSON.stringify(method)}, pattern: new RegExp(${JSON.stringify(regexStr)}), paramNames: ${JSON.stringify(paramNames)}, handler: ${handler} });`);
1270
+ }
1271
+ }
1272
+
1273
+ /**
1274
+ * Generate a wrangler.toml config string for Cloudflare deployments.
1275
+ */
1276
+ static generateWranglerToml(config, name, blockName) {
1277
+ const appName = name || 'app';
1278
+ const today = new Date().toISOString().slice(0, 10);
1279
+ const mainFile = blockName
1280
+ ? '.tova-out/' + appName + '.edge.' + blockName + '.js'
1281
+ : '.tova-out/' + appName + '.edge.js';
1282
+ const lines = [];
1283
+ lines.push('name = "' + appName + '"');
1284
+ lines.push('main = "' + mainFile + '"');
1285
+ lines.push('compatibility_date = "' + today + '"');
1286
+ lines.push('');
1287
+
1288
+ // KV namespaces
1289
+ for (const kv of config.bindings.kv) {
1290
+ lines.push('[[kv_namespaces]]');
1291
+ lines.push('binding = "' + kv.name + '"');
1292
+ lines.push('id = "TODO_' + kv.name + '_ID"');
1293
+ lines.push('');
1294
+ }
1295
+
1296
+ // D1 databases
1297
+ for (const db of config.bindings.sql) {
1298
+ lines.push('[[d1_databases]]');
1299
+ lines.push('binding = "' + db.name + '"');
1300
+ lines.push('database_name = "' + db.name.toLowerCase() + '"');
1301
+ lines.push('database_id = "TODO_' + db.name + '_ID"');
1302
+ lines.push('');
1303
+ }
1304
+
1305
+ // R2 buckets
1306
+ for (const bucket of config.bindings.storage) {
1307
+ lines.push('[[r2_buckets]]');
1308
+ lines.push('binding = "' + bucket.name + '"');
1309
+ lines.push('bucket_name = "' + bucket.name.toLowerCase() + '"');
1310
+ lines.push('');
1311
+ }
1312
+
1313
+ // Queue producers
1314
+ for (const q of config.bindings.queue) {
1315
+ lines.push('[[queues.producers]]');
1316
+ lines.push('binding = "' + q.name + '"');
1317
+ lines.push('queue = "' + q.name.toLowerCase() + '"');
1318
+ lines.push('');
1319
+ }
1320
+
1321
+ // Queue consumers
1322
+ for (const c of config.consumers) {
1323
+ lines.push('[[queues.consumers]]');
1324
+ lines.push('queue = "' + c.queue.toLowerCase() + '"');
1325
+ lines.push('max_batch_size = 10');
1326
+ lines.push('max_batch_timeout = 30');
1327
+ lines.push('');
1328
+ }
1329
+
1330
+ // Cron triggers
1331
+ if (config.schedules.length > 0) {
1332
+ lines.push('[triggers]');
1333
+ const crons = config.schedules.map(s => '"' + s.cron + '"').join(', ');
1334
+ lines.push('crons = [' + crons + ']');
1335
+ lines.push('');
1336
+ }
1337
+
1338
+ // Env vars
1339
+ if (config.envVars.length > 0) {
1340
+ lines.push('[vars]');
1341
+ for (const env of config.envVars) {
1342
+ if (env.defaultValue && env.defaultValue.type === 'StringLiteral') {
1343
+ lines.push(env.name + ' = "' + env.defaultValue.value + '"');
1344
+ }
1345
+ }
1346
+ lines.push('');
1347
+ }
1348
+
1349
+ return lines.join('\n');
1350
+ }
1351
+ }