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.
- package/bin/tova.js +109 -57
- package/package.json +7 -2
- package/src/analyzer/analyzer.js +315 -79
- package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
- package/src/analyzer/form-analyzer.js +113 -0
- package/src/analyzer/scope.js +2 -2
- package/src/codegen/base-codegen.js +1 -0
- package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
- package/src/codegen/cli-codegen.js +386 -0
- package/src/codegen/codegen.js +163 -45
- 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/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 +1 -1
- package/src/parser/ast.js +45 -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/cli-ast.js +35 -0
- package/src/parser/cli-parser.js +140 -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 +86 -53
- package/src/registry/block-registry.js +56 -0
- package/src/registry/plugins/bench-plugin.js +23 -0
- package/src/registry/plugins/browser-plugin.js +30 -0
- package/src/registry/plugins/cli-plugin.js +24 -0
- package/src/registry/plugins/data-plugin.js +21 -0
- package/src/registry/plugins/edge-plugin.js +32 -0
- package/src/registry/plugins/security-plugin.js +27 -0
- package/src/registry/plugins/server-plugin.js +46 -0
- package/src/registry/plugins/shared-plugin.js +17 -0
- package/src/registry/plugins/test-plugin.js +23 -0
- package/src/registry/register-all.js +25 -0
- package/src/runtime/ssr.js +2 -2
- package/src/stdlib/inline.js +178 -3
- 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
|
+
}
|
package/src/codegen/codegen.js
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
|
-
// Main code generator — orchestrates shared/server/
|
|
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 {
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
16
|
-
|
|
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
|
-
|
|
39
|
-
const
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
63
|
-
|
|
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
|
-
|
|
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/
|
|
98
|
-
this._scanBlocksForBuiltins([...serverBlocks, ...
|
|
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
|
|
135
|
+
// Group server and browser blocks by name
|
|
108
136
|
const serverGroups = this._groupByName(serverBlocks);
|
|
109
|
-
const
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
//
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
283
|
+
browser: browserDefault,
|
|
284
|
+
client: browserDefault, // deprecated alias for backward compat
|
|
285
|
+
edge: edges['default'] || '',
|
|
211
286
|
servers, // { "api": code, "ws": code, ... }
|
|
212
|
-
|
|
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) => {
|