tova 0.5.0 → 0.7.0

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 (43) hide show
  1. package/bin/tova.js +109 -57
  2. package/package.json +7 -2
  3. package/src/analyzer/analyzer.js +315 -79
  4. package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
  5. package/src/analyzer/form-analyzer.js +113 -0
  6. package/src/analyzer/scope.js +2 -2
  7. package/src/codegen/base-codegen.js +1 -0
  8. package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
  9. package/src/codegen/cli-codegen.js +386 -0
  10. package/src/codegen/codegen.js +163 -45
  11. package/src/codegen/edge-codegen.js +1351 -0
  12. package/src/codegen/form-codegen.js +553 -0
  13. package/src/codegen/security-codegen.js +5 -5
  14. package/src/codegen/server-codegen.js +88 -7
  15. package/src/diagnostics/error-codes.js +1 -1
  16. package/src/docs/generator.js +1 -1
  17. package/src/formatter/formatter.js +4 -4
  18. package/src/lexer/tokens.js +12 -2
  19. package/src/lsp/server.js +1 -1
  20. package/src/parser/ast.js +45 -5
  21. package/src/parser/{client-ast.js → browser-ast.js} +3 -3
  22. package/src/parser/{client-parser.js → browser-parser.js} +42 -15
  23. package/src/parser/cli-ast.js +35 -0
  24. package/src/parser/cli-parser.js +140 -0
  25. package/src/parser/edge-ast.js +83 -0
  26. package/src/parser/edge-parser.js +262 -0
  27. package/src/parser/form-ast.js +80 -0
  28. package/src/parser/form-parser.js +206 -0
  29. package/src/parser/parser.js +86 -53
  30. package/src/registry/block-registry.js +56 -0
  31. package/src/registry/plugins/bench-plugin.js +23 -0
  32. package/src/registry/plugins/browser-plugin.js +30 -0
  33. package/src/registry/plugins/cli-plugin.js +24 -0
  34. package/src/registry/plugins/data-plugin.js +21 -0
  35. package/src/registry/plugins/edge-plugin.js +32 -0
  36. package/src/registry/plugins/security-plugin.js +27 -0
  37. package/src/registry/plugins/server-plugin.js +46 -0
  38. package/src/registry/plugins/shared-plugin.js +17 -0
  39. package/src/registry/plugins/test-plugin.js +23 -0
  40. package/src/registry/register-all.js +25 -0
  41. package/src/runtime/ssr.js +2 -2
  42. package/src/stdlib/inline.js +178 -3
  43. package/src/version.js +1 -1
@@ -0,0 +1,386 @@
1
+ // CLI code generator for the Tova language
2
+ // Produces a complete zero-dependency CLI executable from cli { } blocks.
3
+
4
+ import { BaseCodegen } from './base-codegen.js';
5
+
6
+ export class CliCodegen extends BaseCodegen {
7
+
8
+ /**
9
+ * Merge all CliBlock nodes into a single config.
10
+ * Multiple cli blocks are merged (last wins on config, commands accumulate).
11
+ */
12
+ static mergeCliBlocks(cliBlocks) {
13
+ const config = {
14
+ name: null,
15
+ version: null,
16
+ description: null,
17
+ };
18
+ const commands = [];
19
+
20
+ for (const block of cliBlocks) {
21
+ for (const field of block.config) {
22
+ if (field.key === 'name' && field.value.type === 'StringLiteral') {
23
+ config.name = field.value.value;
24
+ } else if (field.key === 'version' && field.value.type === 'StringLiteral') {
25
+ config.version = field.value.value;
26
+ } else if (field.key === 'description' && field.value.type === 'StringLiteral') {
27
+ config.description = field.value.value;
28
+ }
29
+ }
30
+ commands.push(...block.commands);
31
+ }
32
+
33
+ return { config, commands };
34
+ }
35
+
36
+ /**
37
+ * Generate a complete CLI executable.
38
+ * @param {Object} cliConfig — merged config from mergeCliBlocks
39
+ * @param {string} sharedCode — shared/top-level compiled code
40
+ * @returns {string} — complete executable JS
41
+ */
42
+ generate(cliConfig, sharedCode) {
43
+ const { config, commands } = cliConfig;
44
+ const lines = [];
45
+
46
+ // Emit shared code (stdlib + top-level)
47
+ if (sharedCode && sharedCode.trim()) {
48
+ lines.push(sharedCode);
49
+ lines.push('');
50
+ }
51
+
52
+ const singleCommand = commands.length === 1;
53
+
54
+ // Emit each command as a function
55
+ for (const cmd of commands) {
56
+ lines.push(this._genCommandFunction(cmd));
57
+ lines.push('');
58
+ }
59
+
60
+ // Generate help functions
61
+ lines.push(this._genMainHelp(config, commands));
62
+ lines.push('');
63
+
64
+ for (const cmd of commands) {
65
+ lines.push(this._genCommandHelp(cmd, config, singleCommand));
66
+ lines.push('');
67
+ }
68
+
69
+ // Generate dispatchers for each command
70
+ for (const cmd of commands) {
71
+ lines.push(this._genCommandDispatcher(cmd));
72
+ lines.push('');
73
+ }
74
+
75
+ // Generate main entry point
76
+ lines.push(this._genMain(config, commands, singleCommand));
77
+ lines.push('');
78
+
79
+ // Auto-invoke
80
+ lines.push('__cli_main(process.argv.slice(2));');
81
+
82
+ return lines.join('\n');
83
+ }
84
+
85
+ /**
86
+ * Generate a command function: __cmd_<name>(params...)
87
+ */
88
+ _genCommandFunction(cmd) {
89
+ const paramNames = cmd.params.map(p => p.name);
90
+ const asyncPrefix = cmd.isAsync ? 'async ' : '';
91
+ const body = this.genBlockStatements(cmd.body);
92
+ return `${asyncPrefix}function __cmd_${cmd.name}(${paramNames.join(', ')}) {\n${body}\n}`;
93
+ }
94
+
95
+ /**
96
+ * Generate overall --help output
97
+ */
98
+ _genMainHelp(config, commands) {
99
+ const lines = [];
100
+ lines.push('function __cli_help() {');
101
+ lines.push(' const lines = [];');
102
+
103
+ if (config.name) {
104
+ if (config.description) {
105
+ lines.push(` lines.push("${config.name} — ${this._escStr(config.description)}");`);
106
+ } else {
107
+ lines.push(` lines.push("${this._escStr(config.name)}");`);
108
+ }
109
+ }
110
+ if (config.version) {
111
+ lines.push(` lines.push("Version: ${this._escStr(config.version)}");`);
112
+ }
113
+
114
+ lines.push(' lines.push("");');
115
+ lines.push(' lines.push("USAGE:");');
116
+
117
+ if (commands.length === 1) {
118
+ const cmd = commands[0];
119
+ const usage = this._buildUsageLine(cmd, config);
120
+ lines.push(` lines.push(" ${usage}");`);
121
+ } else {
122
+ lines.push(` lines.push(" ${config.name || 'cli'} <command> [options]");`);
123
+ lines.push(' lines.push("");');
124
+ lines.push(' lines.push("COMMANDS:");');
125
+
126
+ for (const cmd of commands) {
127
+ const desc = this._getCommandDescription(cmd);
128
+ lines.push(` lines.push(" ${cmd.name.padEnd(16)}${this._escStr(desc)}");`);
129
+ }
130
+ }
131
+
132
+ lines.push(' lines.push("");');
133
+ lines.push(' lines.push("OPTIONS:");');
134
+ lines.push(' lines.push(" --help, -h Show help");');
135
+ if (config.version) {
136
+ lines.push(' lines.push(" --version, -v Show version");');
137
+ }
138
+
139
+ lines.push(' console.log(lines.join("\\n"));');
140
+ lines.push('}');
141
+ return lines.join('\n');
142
+ }
143
+
144
+ /**
145
+ * Generate per-command help: __cli_command_help_<name>()
146
+ */
147
+ _genCommandHelp(cmd, config, singleCommand) {
148
+ const lines = [];
149
+ lines.push(`function __cli_command_help_${cmd.name}() {`);
150
+ lines.push(' const lines = [];');
151
+
152
+ const usage = this._buildUsageLine(cmd, config);
153
+ lines.push(` lines.push("USAGE:");`);
154
+ lines.push(` lines.push(" ${usage}");`);
155
+
156
+ const positionals = cmd.params.filter(p => !p.isFlag);
157
+ const flags = cmd.params.filter(p => p.isFlag);
158
+
159
+ if (positionals.length > 0) {
160
+ lines.push(' lines.push("");');
161
+ lines.push(' lines.push("ARGUMENTS:");');
162
+ for (const p of positionals) {
163
+ const typeSuffix = p.typeAnnotation ? ` <${p.typeAnnotation}>` : '';
164
+ const optSuffix = p.isOptional ? ' (optional)' : '';
165
+ const defSuffix = p.defaultValue ? ` (default: ${this._getDefaultStr(p)})` : '';
166
+ lines.push(` lines.push(" ${p.name.padEnd(16)}${this._escStr(typeSuffix + optSuffix + defSuffix)}");`);
167
+ }
168
+ }
169
+
170
+ if (flags.length > 0) {
171
+ lines.push(' lines.push("");');
172
+ lines.push(' lines.push("OPTIONS:");');
173
+ for (const f of flags) {
174
+ const typePart = f.typeAnnotation === 'Bool' ? '' : (f.typeAnnotation ? ` <${f.typeAnnotation}>` : '');
175
+ const defPart = f.defaultValue ? ` (default: ${this._getDefaultStr(f)})` : '';
176
+ lines.push(` lines.push(" --${f.name.padEnd(14)}${this._escStr(typePart + defPart)}");`);
177
+ }
178
+ }
179
+
180
+ lines.push(' lines.push(" --help, -h".padEnd(18) + "Show help");');
181
+ lines.push(' console.log(lines.join("\\n"));');
182
+ lines.push('}');
183
+ return lines.join('\n');
184
+ }
185
+
186
+ /**
187
+ * Generate argv dispatcher for a command: __cli_dispatch_<name>(argv)
188
+ */
189
+ _genCommandDispatcher(cmd) {
190
+ const lines = [];
191
+ const asyncPrefix = cmd.isAsync ? 'async ' : '';
192
+ lines.push(`${asyncPrefix}function __cli_dispatch_${cmd.name}(argv) {`);
193
+
194
+ const positionals = cmd.params.filter(p => !p.isFlag);
195
+ const flags = cmd.params.filter(p => p.isFlag);
196
+
197
+ // Initialize flag variables with defaults
198
+ for (const f of flags) {
199
+ if (f.typeAnnotation === 'Bool') {
200
+ lines.push(` let __flag_${f.name} = false;`);
201
+ } else if (f.isRepeated) {
202
+ lines.push(` let __flag_${f.name} = [];`);
203
+ } else if (f.defaultValue) {
204
+ lines.push(` let __flag_${f.name} = ${this.genExpression(f.defaultValue)};`);
205
+ } else {
206
+ lines.push(` let __flag_${f.name} = undefined;`);
207
+ }
208
+ }
209
+
210
+ // Positional collector
211
+ lines.push(' const __positionals = [];');
212
+
213
+ // Parse argv
214
+ lines.push(' for (let __i = 0; __i < argv.length; __i++) {');
215
+ lines.push(' const __arg = argv[__i];');
216
+
217
+ // Check --help
218
+ lines.push(` if (__arg === "--help" || __arg === "-h") { __cli_command_help_${cmd.name}(); return; }`);
219
+
220
+ // Check each flag
221
+ for (const f of flags) {
222
+ if (f.typeAnnotation === 'Bool') {
223
+ lines.push(` if (__arg === "--${f.name}") { __flag_${f.name} = true; continue; }`);
224
+ lines.push(` if (__arg === "--no-${f.name}") { __flag_${f.name} = false; continue; }`);
225
+ } else if (f.isRepeated) {
226
+ lines.push(` if (__arg === "--${f.name}") {`);
227
+ lines.push(` if (__i + 1 >= argv.length) { console.error("Error: --${f.name} requires a value"); process.exit(1); }`);
228
+ lines.push(` __flag_${f.name}.push(${this._genCoercion(`argv[++__i]`, f.typeAnnotation, f.name)});`);
229
+ lines.push(' continue;');
230
+ lines.push(' }');
231
+ } else {
232
+ lines.push(` if (__arg === "--${f.name}") {`);
233
+ lines.push(` if (__i + 1 >= argv.length) { console.error("Error: --${f.name} requires a value"); process.exit(1); }`);
234
+ lines.push(` __flag_${f.name} = ${this._genCoercion(`argv[++__i]`, f.typeAnnotation, f.name)};`);
235
+ lines.push(' continue;');
236
+ lines.push(' }');
237
+ // Support --flag=value syntax
238
+ lines.push(` if (__arg.startsWith("--${f.name}=")) {`);
239
+ lines.push(` __flag_${f.name} = ${this._genCoercion(`__arg.slice(${f.name.length + 3})`, f.typeAnnotation, f.name)};`);
240
+ lines.push(' continue;');
241
+ lines.push(' }');
242
+ }
243
+ }
244
+
245
+ // Unknown flags
246
+ lines.push(' if (__arg.startsWith("--")) { console.error("Error: Unknown flag " + __arg); process.exit(1); }');
247
+
248
+ // Collect positionals
249
+ lines.push(' __positionals.push(__arg);');
250
+ lines.push(' }');
251
+
252
+ // Validate and assign positionals
253
+ for (let i = 0; i < positionals.length; i++) {
254
+ const p = positionals[i];
255
+ if (!p.isOptional && !p.defaultValue) {
256
+ lines.push(` if (__positionals.length <= ${i}) {`);
257
+ lines.push(` console.error("Error: Missing required argument <${p.name}>");`);
258
+ lines.push(` __cli_command_help_${cmd.name}();`);
259
+ lines.push(' process.exit(1);');
260
+ lines.push(' }');
261
+ }
262
+ }
263
+
264
+ // Build call arguments
265
+ const callArgs = [];
266
+ for (const p of cmd.params) {
267
+ if (p.isFlag) {
268
+ callArgs.push(`__flag_${p.name}`);
269
+ } else {
270
+ const idx = positionals.indexOf(p);
271
+ if (p.isOptional || p.defaultValue) {
272
+ const def = p.defaultValue ? this.genExpression(p.defaultValue) : 'undefined';
273
+ callArgs.push(`__positionals.length > ${idx} ? ${this._genCoercion(`__positionals[${idx}]`, p.typeAnnotation, p.name)} : ${def}`);
274
+ } else {
275
+ callArgs.push(this._genCoercion(`__positionals[${idx}]`, p.typeAnnotation, p.name));
276
+ }
277
+ }
278
+ }
279
+
280
+ const awaitPrefix = cmd.isAsync ? 'await ' : '';
281
+ lines.push(` ${awaitPrefix}__cmd_${cmd.name}(${callArgs.join(', ')});`);
282
+ lines.push('}');
283
+ return lines.join('\n');
284
+ }
285
+
286
+ /**
287
+ * Generate main entry point
288
+ */
289
+ _genMain(config, commands, singleCommand) {
290
+ const lines = [];
291
+ lines.push('async function __cli_main(argv) {');
292
+
293
+ if (singleCommand) {
294
+ const cmd = commands[0];
295
+ // Single-command mode: no subcommand routing
296
+ lines.push(' if (argv.includes("--help") || argv.includes("-h")) { __cli_help(); return; }');
297
+ if (config.version) {
298
+ lines.push(` if (argv.includes("--version") || argv.includes("-v")) { console.log("${this._escStr(config.version)}"); return; }`);
299
+ }
300
+ lines.push(` await __cli_dispatch_${cmd.name}(argv);`);
301
+ } else {
302
+ // Multi-command: subcommand routing
303
+ lines.push(' if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") { __cli_help(); return; }');
304
+ if (config.version) {
305
+ lines.push(` if (argv[0] === "--version" || argv[0] === "-v") { console.log("${this._escStr(config.version)}"); return; }`);
306
+ }
307
+ lines.push(' const __subcmd = argv[0];');
308
+ lines.push(' const __subargv = argv.slice(1);');
309
+ lines.push(' switch (__subcmd) {');
310
+ for (const cmd of commands) {
311
+ lines.push(` case "${cmd.name}": await __cli_dispatch_${cmd.name}(__subargv); break;`);
312
+ }
313
+ lines.push(' default:');
314
+ lines.push(' console.error("Error: Unknown command \\"" + __subcmd + "\\"");');
315
+ lines.push(' __cli_help();');
316
+ lines.push(' process.exit(1);');
317
+ lines.push(' }');
318
+ }
319
+
320
+ lines.push('}');
321
+ return lines.join('\n');
322
+ }
323
+
324
+ /**
325
+ * Generate type coercion code for argv string → target type
326
+ */
327
+ _genCoercion(expr, type, name) {
328
+ switch (type) {
329
+ case 'Int':
330
+ return `(function(v) { const n = parseInt(v, 10); if (isNaN(n)) { console.error("Error: --${name} must be an integer, got \\"" + v + "\\""); process.exit(1); } return n; })(${expr})`;
331
+ case 'Float':
332
+ return `(function(v) { const n = parseFloat(v); if (isNaN(n)) { console.error("Error: --${name} must be a number, got \\"" + v + "\\""); process.exit(1); } return n; })(${expr})`;
333
+ case 'Bool':
334
+ return `(${expr} === "true" || ${expr} === "1" || ${expr} === "yes")`;
335
+ case 'String':
336
+ default:
337
+ return expr;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Build a usage line for a command
343
+ */
344
+ _buildUsageLine(cmd, config) {
345
+ const prefix = config.name || 'cli';
346
+ const parts = [prefix];
347
+ // Only show command name if multi-command
348
+ // (Caller should decide; we always include for per-command help)
349
+ parts.push(cmd.name);
350
+
351
+ for (const p of cmd.params) {
352
+ if (p.isFlag) {
353
+ if (p.typeAnnotation === 'Bool') {
354
+ parts.push(`[--${p.name}]`);
355
+ } else {
356
+ parts.push(`[--${p.name} <${p.typeAnnotation || 'value'}>]`);
357
+ }
358
+ } else {
359
+ if (p.isOptional || p.defaultValue) {
360
+ parts.push(`[${p.name}]`);
361
+ } else {
362
+ parts.push(`<${p.name}>`);
363
+ }
364
+ }
365
+ }
366
+ return parts.join(' ');
367
+ }
368
+
369
+ _getCommandDescription(cmd) {
370
+ // Could be extended to parse docstrings — for now, just the command name
371
+ return '';
372
+ }
373
+
374
+ _getDefaultStr(param) {
375
+ if (!param.defaultValue) return '';
376
+ if (param.defaultValue.type === 'StringLiteral') return `"${param.defaultValue.value}"`;
377
+ if (param.defaultValue.type === 'NumberLiteral') return String(param.defaultValue.value);
378
+ if (param.defaultValue.type === 'BooleanLiteral') return String(param.defaultValue.value);
379
+ return '...';
380
+ }
381
+
382
+ _escStr(s) {
383
+ if (!s) return '';
384
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
385
+ }
386
+ }
@@ -1,19 +1,39 @@
1
- // Main code generator — orchestrates shared/server/client codegen
1
+ // Main code generator — orchestrates shared/server/browser codegen
2
2
  // Supports named multi-blocks: server "api" { }, server "ws" { }
3
3
  // Blocks with the same name are merged; different names produce separate output files.
4
4
 
5
+ import { createRequire } from 'module';
5
6
  import { SharedCodegen } from './shared-codegen.js';
6
7
  import { BUILTIN_NAMES } from '../stdlib/inline.js';
7
- import { ServerCodegen } from './server-codegen.js';
8
- import { ClientCodegen } from './client-codegen.js';
9
- import { SecurityCodegen } from './security-codegen.js';
8
+ import { BlockRegistry } from '../registry/register-all.js';
9
+
10
+ // Lazy-load domain-specific codegens so pure scripts don't pay for them
11
+ const _require = createRequire(import.meta.url);
12
+ let _ServerCodegen, _BrowserCodegen, _SecurityCodegen, _CliCodegen, _EdgeCodegen;
10
13
 
11
14
  function getServerCodegen() {
12
- return ServerCodegen;
15
+ if (!_ServerCodegen) _ServerCodegen = _require('./server-codegen.js').ServerCodegen;
16
+ return _ServerCodegen;
17
+ }
18
+
19
+ function getBrowserCodegen() {
20
+ if (!_BrowserCodegen) _BrowserCodegen = _require('./browser-codegen.js').BrowserCodegen;
21
+ return _BrowserCodegen;
13
22
  }
14
23
 
15
- function getClientCodegen() {
16
- return ClientCodegen;
24
+ function getSecurityCodegen() {
25
+ if (!_SecurityCodegen) _SecurityCodegen = _require('./security-codegen.js').SecurityCodegen;
26
+ return _SecurityCodegen;
27
+ }
28
+
29
+ function getCliCodegen() {
30
+ if (!_CliCodegen) _CliCodegen = _require('./cli-codegen.js').CliCodegen;
31
+ return _CliCodegen;
32
+ }
33
+
34
+ function getEdgeCodegen() {
35
+ if (!_EdgeCodegen) _EdgeCodegen = _require('./edge-codegen.js').EdgeCodegen;
36
+ return _EdgeCodegen;
17
37
  }
18
38
 
19
39
  export class CodeGenerator {
@@ -35,35 +55,42 @@ export class CodeGenerator {
35
55
  }
36
56
 
37
57
  generate() {
38
- const sharedBlocks = [];
39
- const serverBlocks = [];
40
- const clientBlocks = [];
58
+ // Registry-driven block sorting
59
+ const blocksByType = new Map();
41
60
  const topLevel = [];
42
61
 
43
- const testBlocks = [];
44
- const benchBlocks = [];
45
- const dataBlocks = [];
46
- const securityBlocks = [];
47
-
48
62
  for (const node of this.ast.body) {
49
- switch (node.type) {
50
- case 'SharedBlock': sharedBlocks.push(node); break;
51
- case 'ServerBlock': serverBlocks.push(node); break;
52
- case 'ClientBlock': clientBlocks.push(node); break;
53
- case 'TestBlock': testBlocks.push(node); break;
54
- case 'BenchBlock': benchBlocks.push(node); break;
55
- case 'DataBlock': dataBlocks.push(node); break;
56
- case 'SecurityBlock': securityBlocks.push(node); break;
57
- default: topLevel.push(node); break;
63
+ const plugin = BlockRegistry.getByAstType(node.type);
64
+ if (plugin) {
65
+ if (!blocksByType.has(plugin.name)) blocksByType.set(plugin.name, []);
66
+ blocksByType.get(plugin.name).push(node);
67
+ } else {
68
+ topLevel.push(node);
58
69
  }
59
70
  }
60
71
 
72
+ const getBlocks = (name) => blocksByType.get(name) || [];
73
+
74
+ // Early-return blocks (e.g., CLI mode)
75
+ for (const plugin of BlockRegistry.all()) {
76
+ if (plugin.codegen?.earlyReturn && getBlocks(plugin.name).length > 0) {
77
+ return this[plugin.codegen.earlyReturnMethod](getBlocks(plugin.name), topLevel);
78
+ }
79
+ }
80
+
81
+ // Convenience aliases
82
+ const sharedBlocks = getBlocks('shared');
83
+ const serverBlocks = getBlocks('server');
84
+ const browserBlocks = getBlocks('browser');
85
+ const testBlocks = getBlocks('test');
86
+ const benchBlocks = getBlocks('bench');
87
+ const dataBlocks = getBlocks('data');
88
+ const securityBlocks = getBlocks('security');
89
+ const edgeBlocks = getBlocks('edge');
90
+
61
91
  // Detect module mode: no blocks, only top-level statements
62
- const isModule = sharedBlocks.length === 0 && serverBlocks.length === 0
63
- && clientBlocks.length === 0 && testBlocks.length === 0
64
- && benchBlocks.length === 0 && dataBlocks.length === 0
65
- && securityBlocks.length === 0
66
- && topLevel.length > 0;
92
+ const hasAnyBlocks = BlockRegistry.all().some(p => getBlocks(p.name).length > 0);
93
+ const isModule = !hasAnyBlocks && topLevel.length > 0;
67
94
 
68
95
  if (isModule) {
69
96
  const moduleGen = new SharedCodegen();
@@ -77,7 +104,8 @@ export class CodeGenerator {
77
104
  return {
78
105
  shared: combined,
79
106
  server: '',
80
- client: '',
107
+ browser: '',
108
+ client: '', // deprecated alias
81
109
  isModule: true,
82
110
  sourceMappings: moduleGen.getSourceMappings(),
83
111
  _sourceFile: this.filename,
@@ -94,8 +122,8 @@ export class CodeGenerator {
94
122
  ? sharedGen.genBlockStatements({ type: 'BlockStatement', body: topLevel })
95
123
  : '';
96
124
 
97
- // Pre-scan server/client blocks for builtin usage so shared stdlib includes them
98
- this._scanBlocksForBuiltins([...serverBlocks, ...clientBlocks], sharedGen._usedBuiltins);
125
+ // Pre-scan server/browser blocks for builtin usage so shared stdlib includes them
126
+ this._scanBlocksForBuiltins([...serverBlocks, ...browserBlocks, ...edgeBlocks], sharedGen._usedBuiltins);
99
127
 
100
128
  const helpers = sharedGen.generateHelpers();
101
129
 
@@ -104,9 +132,9 @@ export class CodeGenerator {
104
132
 
105
133
  const combinedShared = [helpers, sharedCode, topLevelCode, dataCode].filter(s => s.trim()).join('\n').trim();
106
134
 
107
- // Group server and client blocks by name
135
+ // Group server and browser blocks by name
108
136
  const serverGroups = this._groupByName(serverBlocks);
109
- const clientGroups = this._groupByName(clientBlocks);
137
+ const browserGroups = this._groupByName(browserBlocks);
110
138
 
111
139
  // Collect function names per named server block for inter-server RPC
112
140
  const serverFunctionMap = new Map(); // blockName -> [fnName, ...]
@@ -133,7 +161,7 @@ export class CodeGenerator {
133
161
 
134
162
  // Merge security blocks into a single config
135
163
  const securityConfig = securityBlocks.length > 0
136
- ? SecurityCodegen.mergeSecurityBlocks(securityBlocks)
164
+ ? getSecurityCodegen().mergeSecurityBlocks(securityBlocks)
137
165
  : null;
138
166
 
139
167
  // Generate server outputs (one per named group)
@@ -152,16 +180,56 @@ export class CodeGenerator {
152
180
  }
153
181
  }
154
182
  }
155
- servers[key] = gen.generate(blocks, combinedShared, name, peerBlocks, sharedBlocks, securityConfig);
183
+ // Include top-level statements so _collectTypes finds top-level type declarations
184
+ const allSharedBlocks = topLevel.length > 0
185
+ ? [...sharedBlocks, { type: 'BlockStatement', body: topLevel }]
186
+ : sharedBlocks;
187
+ servers[key] = gen.generate(blocks, combinedShared, name, peerBlocks, allSharedBlocks, securityConfig);
156
188
  }
157
189
 
158
- // Generate client outputs (one per named group)
159
- const clients = {};
160
- for (const [name, blocks] of clientGroups) {
161
- const gen = new (getClientCodegen())();
190
+ // Collect type validators from shared blocks and top-level for form type inheritance
191
+ const typeValidatorsMap = {};
192
+ const _collectTypeValidators = (stmts) => {
193
+ for (const stmt of stmts) {
194
+ if (stmt.type === 'TypeDeclaration' && stmt.variants) {
195
+ const fields = [];
196
+ for (const v of stmt.variants) {
197
+ if (v.type === 'TypeField' && v.validators && v.validators.length > 0) {
198
+ fields.push({ name: v.name, validators: v.validators });
199
+ }
200
+ }
201
+ if (fields.length > 0) {
202
+ typeValidatorsMap[stmt.name] = { fields };
203
+ }
204
+ }
205
+ }
206
+ };
207
+ for (const sb of sharedBlocks) {
208
+ _collectTypeValidators(sb.body);
209
+ }
210
+ _collectTypeValidators(topLevel);
211
+
212
+ // Generate browser outputs (one per named group)
213
+ const browsers = {};
214
+ for (const [name, blocks] of browserGroups) {
215
+ const gen = new (getBrowserCodegen())();
162
216
  gen._sourceMapsEnabled = this._sourceMaps;
163
217
  const key = name || 'default';
164
- clients[key] = gen.generate(blocks, combinedShared, sharedGen._usedBuiltins, securityConfig);
218
+ browsers[key] = gen.generate(blocks, combinedShared, sharedGen._usedBuiltins, securityConfig, typeValidatorsMap);
219
+ }
220
+
221
+ // Generate edge outputs (one per named group)
222
+ const edges = {};
223
+ if (edgeBlocks.length > 0) {
224
+ const edgeGroups = this._groupByName(edgeBlocks);
225
+ for (const [name, blocks] of edgeGroups) {
226
+ const Edge = getEdgeCodegen();
227
+ const gen = new Edge();
228
+ gen._sourceMapsEnabled = this._sourceMaps;
229
+ const key = name || 'default';
230
+ const edgeConfig = Edge.mergeEdgeBlocks(blocks);
231
+ edges[key] = gen.generate(edgeConfig, combinedShared, securityConfig);
232
+ }
165
233
  }
166
234
 
167
235
  // Generate tests if test blocks exist
@@ -185,16 +253,20 @@ export class CodeGenerator {
185
253
  }
186
254
 
187
255
  // Backward-compatible: if only unnamed blocks, return flat structure
188
- const hasNamedBlocks = [...serverGroups.keys(), ...clientGroups.keys()].some(k => k !== null);
256
+ const edgeGroupKeys = edgeBlocks.length > 0 ? [...this._groupByName(edgeBlocks).keys()] : [];
257
+ const hasNamedBlocks = [...serverGroups.keys(), ...browserGroups.keys(), ...edgeGroupKeys].some(k => k !== null);
189
258
 
190
259
  // Collect source mappings from all codegens
191
260
  const sourceMappings = sharedGen.getSourceMappings();
192
261
 
193
262
  if (!hasNamedBlocks) {
263
+ const browserCode = browsers['default'] || '';
194
264
  const result = {
195
265
  shared: combinedShared,
196
266
  server: servers['default'] || '',
197
- client: clients['default'] || '',
267
+ browser: browserCode,
268
+ client: browserCode, // deprecated alias for backward compat
269
+ edge: edges['default'] || '',
198
270
  sourceMappings,
199
271
  _sourceFile: this.filename,
200
272
  };
@@ -204,12 +276,17 @@ export class CodeGenerator {
204
276
  }
205
277
 
206
278
  // Multi-block output: separate files per named block
279
+ const browserDefault = browsers['default'] || '';
207
280
  const result = {
208
281
  shared: combinedShared,
209
282
  server: servers['default'] || '',
210
- client: clients['default'] || '',
283
+ browser: browserDefault,
284
+ client: browserDefault, // deprecated alias for backward compat
285
+ edge: edges['default'] || '',
211
286
  servers, // { "api": code, "ws": code, ... }
212
- clients, // { "admin": code, "dashboard": code, ... }
287
+ browsers, // { "admin": code, "dashboard": code, ... }
288
+ clients: browsers, // deprecated alias for backward compat
289
+ edges, // { "api": code, "assets": code, ... }
213
290
  multiBlock: true,
214
291
  sourceMappings,
215
292
  _sourceFile: this.filename,
@@ -219,6 +296,47 @@ export class CodeGenerator {
219
296
  return result;
220
297
  }
221
298
 
299
+ // Generate CLI executable from cli {} blocks
300
+ _generateCli(cliBlocks, topLevel) {
301
+ const sharedGen = new SharedCodegen();
302
+ sharedGen._sourceMapsEnabled = this._sourceMaps;
303
+ sharedGen.setSourceFile(this.filename);
304
+
305
+ // Generate top-level code (shared helpers, type declarations, etc.)
306
+ const topLevelCode = topLevel.length > 0
307
+ ? sharedGen.genBlockStatements({ type: 'BlockStatement', body: topLevel })
308
+ : '';
309
+
310
+ // Scan cli command bodies for builtin usage
311
+ for (const block of cliBlocks) {
312
+ for (const cmd of block.commands) {
313
+ this._scanBlocksForBuiltins([cmd.body], sharedGen._usedBuiltins);
314
+ }
315
+ }
316
+ // Also scan top-level for builtins
317
+ this._scanBlocksForBuiltins(topLevel, sharedGen._usedBuiltins);
318
+
319
+ const helpers = sharedGen.generateHelpers();
320
+ const combinedShared = [helpers, topLevelCode].filter(s => s.trim()).join('\n').trim();
321
+
322
+ const Cli = getCliCodegen();
323
+ const cliConfig = Cli.mergeCliBlocks(cliBlocks);
324
+ const cliGen = new Cli();
325
+ cliGen._sourceMapsEnabled = this._sourceMaps;
326
+ const cliCode = cliGen.generate(cliConfig, combinedShared);
327
+
328
+ return {
329
+ cli: cliCode,
330
+ isCli: true,
331
+ shared: '',
332
+ server: '',
333
+ browser: '',
334
+ client: '', // deprecated alias
335
+ sourceMappings: sharedGen.getSourceMappings(),
336
+ _sourceFile: this.filename,
337
+ };
338
+ }
339
+
222
340
  // Walk AST nodes to find builtin function calls/identifiers
223
341
  _scanBlocksForBuiltins(blocks, targetSet) {
224
342
  const walk = (node) => {