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.
- package/bin/tova.js +261 -60
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +351 -11
- package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/form-analyzer.js +113 -0
- package/src/analyzer/scope.js +2 -2
- package/src/codegen/base-codegen.js +1160 -10
- package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
- package/src/codegen/codegen.js +119 -28
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/edge-codegen.js +1351 -0
- package/src/codegen/form-codegen.js +553 -0
- package/src/codegen/security-codegen.js +5 -5
- package/src/codegen/server-codegen.js +88 -7
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/wasm-codegen.js +6 -0
- package/src/config/edit-toml.js +6 -2
- package/src/config/git-resolver.js +128 -0
- package/src/config/lock-file.js +57 -0
- package/src/config/module-cache.js +58 -0
- package/src/config/module-entry.js +37 -0
- package/src/config/module-path.js +31 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +17 -0
- package/src/config/resolver.js +139 -0
- package/src/config/search.js +28 -0
- package/src/config/semver.js +72 -0
- package/src/config/toml.js +48 -5
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +311 -0
- package/src/diagnostics/error-codes.js +1 -1
- package/src/docs/generator.js +1 -1
- package/src/formatter/formatter.js +4 -4
- package/src/lexer/tokens.js +12 -2
- package/src/lsp/server.js +483 -1
- package/src/parser/ast.js +60 -5
- package/src/parser/{client-ast.js → browser-ast.js} +3 -3
- package/src/parser/{client-parser.js → browser-parser.js} +42 -15
- package/src/parser/concurrency-ast.js +15 -0
- package/src/parser/concurrency-parser.js +236 -0
- package/src/parser/deploy-ast.js +37 -0
- package/src/parser/deploy-parser.js +132 -0
- package/src/parser/edge-ast.js +83 -0
- package/src/parser/edge-parser.js +262 -0
- package/src/parser/form-ast.js +80 -0
- package/src/parser/form-parser.js +206 -0
- package/src/parser/parser.js +82 -14
- package/src/parser/select-ast.js +39 -0
- package/src/registry/plugins/browser-plugin.js +30 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/edge-plugin.js +32 -0
- package/src/registry/register-all.js +8 -2
- package/src/runtime/ssr.js +2 -2
- package/src/stdlib/inline.js +38 -6
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/version.js +1 -1
- 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
|
+
}
|