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
@@ -1,8 +1,7 @@
1
- import { TokenType } from '../lexer/tokens.js';
1
+ import { TokenType, Keywords } from '../lexer/tokens.js';
2
2
  import * as AST from './ast.js';
3
- import { installServerParser } from './server-parser.js';
4
- import { installClientParser } from './client-parser.js';
5
- import { installSecurityParser } from './security-parser.js';
3
+ import { FormValidator } from './form-ast.js';
4
+ import { BlockRegistry } from '../registry/register-all.js';
6
5
 
7
6
  export class Parser {
8
7
  static MAX_EXPRESSION_DEPTH = 200;
@@ -77,6 +76,16 @@ export class Parser {
77
76
  this.error(message || `Expected ${type}, got ${this.current().type}`);
78
77
  }
79
78
 
79
+ // Accept IDENTIFIER or any keyword token as a property name (e.g., obj.field, obj.state).
80
+ // Keywords are valid property names after '.' and '?.' just like in JavaScript.
81
+ expectPropertyName(message) {
82
+ const tok = this.current();
83
+ if (tok.type === TokenType.IDENTIFIER || (typeof tok.value === 'string' && tok.type !== TokenType.EOF && tok.type !== TokenType.NUMBER && tok.type !== TokenType.STRING && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(tok.value))) {
84
+ return this.advance();
85
+ }
86
+ this.error(message || `Expected property name, got ${tok.type}`);
87
+ }
88
+
80
89
  loc() {
81
90
  const tok = this.current();
82
91
  return { line: tok.line, column: tok.column, file: this.filename };
@@ -97,7 +106,7 @@ export class Parser {
97
106
  tok.type === TokenType.WHILE || tok.type === TokenType.RETURN ||
98
107
  tok.type === TokenType.IMPORT || tok.type === TokenType.MATCH ||
99
108
  tok.type === TokenType.TRY || tok.type === TokenType.SERVER ||
100
- tok.type === TokenType.CLIENT || tok.type === TokenType.SHARED ||
109
+ tok.type === TokenType.BROWSER || tok.type === TokenType.SHARED ||
101
110
  tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
102
111
  tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
103
112
  tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
@@ -146,7 +155,7 @@ export class Parser {
146
155
  tok.type === TokenType.WHILE || tok.type === TokenType.RETURN ||
147
156
  tok.type === TokenType.IMPORT || tok.type === TokenType.MATCH ||
148
157
  tok.type === TokenType.TRY || tok.type === TokenType.SERVER ||
149
- tok.type === TokenType.CLIENT || tok.type === TokenType.SHARED ||
158
+ tok.type === TokenType.BROWSER || tok.type === TokenType.SHARED ||
150
159
  tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
151
160
  tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
152
161
  tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
@@ -165,7 +174,8 @@ export class Parser {
165
174
  const next = this.peek(1);
166
175
  // Fragment: <>
167
176
  if (next.type === TokenType.GREATER) return true;
168
- if (next.type !== TokenType.IDENTIFIER) return false;
177
+ // Accept identifiers and keywords as JSX tag names (e.g., <form>, <label>, <field>)
178
+ if (next.type !== TokenType.IDENTIFIER && !(next.value in Keywords)) return false;
169
179
  // Uppercase tag is always a component reference, never a comparison variable
170
180
  if (/^[A-Z]/.test(next.value)) return true;
171
181
  const afterIdent = this.peek(2);
@@ -263,10 +273,11 @@ export class Parser {
263
273
  const doc = docsByEndLine.get(node.loc.line - 1);
264
274
  if (doc) node.docstring = doc;
265
275
  }
266
- // Walk into blocks
267
- if (node.body && Array.isArray(node.body)) walk(node.body);
268
- if (node.type === 'ServerBlock' || node.type === 'ClientBlock' || node.type === 'SharedBlock') {
269
- if (node.body) walk(node.body);
276
+ // Walk into block bodies (arrays) and block nodes with body properties
277
+ if (node.body && Array.isArray(node.body)) {
278
+ walk(node.body);
279
+ } else if (BlockRegistry.getByAstType(node.type) && node.body) {
280
+ walk(node.body);
270
281
  }
271
282
  }
272
283
  };
@@ -274,46 +285,30 @@ export class Parser {
274
285
  }
275
286
 
276
287
  parseTopLevel() {
277
- if (this.check(TokenType.SERVER)) {
278
- if (!Parser.prototype._serverParserInstalled) {
279
- installServerParser(Parser);
280
- }
281
- return this.parseServerBlock();
282
- }
283
- if (this.check(TokenType.CLIENT)) {
284
- if (!Parser.prototype._clientParserInstalled) {
285
- installClientParser(Parser);
288
+ // Registry-driven block dispatch
289
+ for (const plugin of BlockRegistry.all()) {
290
+ if (this._matchesBlock(plugin)) {
291
+ const p = plugin.parser;
292
+ if (p.install && p.installedFlag && !Parser.prototype[p.installedFlag]) {
293
+ p.install(Parser);
294
+ }
295
+ return this[p.method]();
286
296
  }
287
- return this.parseClientBlock();
288
297
  }
289
- if (this.check(TokenType.SHARED)) return this.parseSharedBlock();
290
298
  if (this.check(TokenType.IMPORT)) return this.parseImport();
291
- // data block: data { ... }
292
- if (this.check(TokenType.IDENTIFIER) && this.current().value === 'data' && this.peek(1).type === TokenType.LBRACE) {
293
- return this.parseDataBlock();
294
- }
295
- // security block: security { ... }
296
- if (this.check(TokenType.IDENTIFIER) && this.current().value === 'security' && this.peek(1).type === TokenType.LBRACE) {
297
- if (!Parser.prototype._securityParserInstalled) {
298
- installSecurityParser(Parser);
299
- }
300
- return this.parseSecurityBlock();
301
- }
302
- // test block: test "name" { ... } or test { ... }
303
- if (this.check(TokenType.IDENTIFIER) && this.current().value === 'test') {
304
- const next = this.peek(1);
305
- if (next.type === TokenType.LBRACE || next.type === TokenType.STRING) {
306
- return this.parseTestBlock();
307
- }
299
+ return this.parseStatement();
300
+ }
301
+
302
+ _matchesBlock(plugin) {
303
+ const d = plugin.detection;
304
+ if (d.strategy === 'keyword') {
305
+ return this.check(TokenType[d.tokenType]);
308
306
  }
309
- // bench block: bench "name" { ... } or bench { ... }
310
- if (this.check(TokenType.IDENTIFIER) && this.current().value === 'bench') {
311
- const next = this.peek(1);
312
- if (next.type === TokenType.LBRACE || next.type === TokenType.STRING) {
313
- return this.parseBenchBlock();
314
- }
307
+ if (d.strategy === 'identifier') {
308
+ if (!this.check(TokenType.IDENTIFIER) || this.current().value !== d.identifierValue) return false;
309
+ return d.lookahead ? d.lookahead(this) : this.peek(1).type === TokenType.LBRACE;
315
310
  }
316
- return this.parseStatement();
311
+ return false;
317
312
  }
318
313
 
319
314
  parseTestBlock() {
@@ -395,7 +390,7 @@ export class Parser {
395
390
  }
396
391
 
397
392
  // ─── Full-stack blocks ────────────────────────────────────
398
- // parseClientBlock() and client-specific methods are in client-parser.js (lazy-loaded)
393
+ // parseBrowserBlock() and browser-specific methods are in browser-parser.js (lazy-loaded)
399
394
 
400
395
  parseSharedBlock() {
401
396
  const l = this.loc();
@@ -538,7 +533,7 @@ export class Parser {
538
533
  return new AST.RefreshPolicy(sourceName, { value, unit }, l);
539
534
  }
540
535
 
541
- // Client-specific statements and JSX parsing are in client-parser.js (lazy-loaded)
536
+ // Browser-specific statements and JSX parsing are in browser-parser.js (lazy-loaded)
542
537
 
543
538
  // ─── Statements ───────────────────────────────────────────
544
539
 
@@ -997,6 +992,43 @@ export class Parser {
997
992
  return new AST.TypeAnnotation(name, typeParams, l);
998
993
  }
999
994
 
995
+ // Parse inline validators for type fields: { required, email, min(18) }
996
+ // Uses comma-separated validator names, supports args in parens
997
+ _parseTypeFieldValidators() {
998
+ const validators = [];
999
+ if (this.check(TokenType.LBRACE)) {
1000
+ this.advance(); // consume {
1001
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
1002
+ validators.push(this._parseInlineValidator());
1003
+ this.match(TokenType.COMMA); // optional comma separator
1004
+ }
1005
+ this.expect(TokenType.RBRACE, "Expected '}' to close validator block");
1006
+ }
1007
+ return validators;
1008
+ }
1009
+
1010
+ // Parse a single inline validator: name or name(args...)
1011
+ _parseInlineValidator() {
1012
+ const l = this.loc();
1013
+ let isAsync = false;
1014
+ if (this.check(TokenType.ASYNC)) {
1015
+ isAsync = true;
1016
+ this.advance();
1017
+ }
1018
+ const name = this.expect(TokenType.IDENTIFIER, "Expected validator name").value;
1019
+ const args = [];
1020
+ if (this.match(TokenType.LPAREN)) {
1021
+ if (!this.check(TokenType.RPAREN)) {
1022
+ args.push(this.parseExpression());
1023
+ while (this.match(TokenType.COMMA)) {
1024
+ args.push(this.parseExpression());
1025
+ }
1026
+ }
1027
+ this.expect(TokenType.RPAREN, "Expected ')' after validator arguments");
1028
+ }
1029
+ return new FormValidator(name, args, isAsync, l);
1030
+ }
1031
+
1000
1032
  parseTypeDeclaration() {
1001
1033
  const l = this.loc();
1002
1034
  this.expect(TokenType.TYPE);
@@ -1083,9 +1115,10 @@ export class Parser {
1083
1115
  this.expect(TokenType.RPAREN, "Expected ')' after variant fields");
1084
1116
  variants.push(new AST.TypeVariant(vname, fields, vl));
1085
1117
  } else if (this.match(TokenType.COLON)) {
1086
- // Simple field: name: String
1118
+ // Simple field: name: String or name: String { required, email }
1087
1119
  const ftype = this.parseTypeAnnotation();
1088
- variants.push(new AST.TypeField(vname, ftype, vl));
1120
+ const validators = this._parseTypeFieldValidators();
1121
+ variants.push(new AST.TypeField(vname, ftype, vl, validators));
1089
1122
  } else {
1090
1123
  // Bare variant: None
1091
1124
  variants.push(new AST.TypeVariant(vname, [], vl));
@@ -1761,7 +1794,7 @@ export class Parser {
1761
1794
  expr = new AST.MemberExpression(expr, new AST.NumberLiteral(idx, l), true, l);
1762
1795
  continue;
1763
1796
  }
1764
- const prop = this.expect(TokenType.IDENTIFIER, "Expected property name after '.'").value;
1797
+ const prop = this.expectPropertyName("Expected property name after '.'").value;
1765
1798
  expr = new AST.MemberExpression(expr, prop, false, l);
1766
1799
  continue;
1767
1800
  }
@@ -1769,7 +1802,7 @@ export class Parser {
1769
1802
  if (this.check(TokenType.QUESTION_DOT)) {
1770
1803
  const l = this.loc();
1771
1804
  this.advance();
1772
- const prop = this.expect(TokenType.IDENTIFIER, "Expected property name after '?.'").value;
1805
+ const prop = this.expectPropertyName("Expected property name after '?.'").value;
1773
1806
  expr = new AST.OptionalChain(expr, prop, false, l);
1774
1807
  continue;
1775
1808
  }
@@ -1967,7 +2000,7 @@ export class Parser {
1967
2000
  return this.parseParenOrArrowLambda();
1968
2001
 
1969
2002
  case TokenType.SERVER:
1970
- case TokenType.CLIENT:
2003
+ case TokenType.BROWSER:
1971
2004
  case TokenType.SHARED:
1972
2005
  case TokenType.DERIVE:
1973
2006
  return new AST.Identifier(this.advance().value, l);
@@ -0,0 +1,56 @@
1
+ // Block Registry — central registry for block-type plugins.
2
+ // Each plugin describes how to detect, parse, analyze, and codegen a block type.
3
+
4
+ const _plugins = new Map(); // name → plugin
5
+ const _astTypeMap = new Map(); // astNodeType → plugin | _NOOP_SENTINEL
6
+ const _order = []; // registration order
7
+
8
+ // Sentinel value for noop AST types (returned by getByAstType to avoid a second lookup)
9
+ const _NOOP_SENTINEL = Object.freeze({ __noop: true });
10
+
11
+ export const BlockRegistry = {
12
+ NOOP: _NOOP_SENTINEL,
13
+
14
+ register(plugin) {
15
+ if (_plugins.has(plugin.name)) {
16
+ throw new Error(`Block plugin "${plugin.name}" already registered`);
17
+ }
18
+ _plugins.set(plugin.name, plugin);
19
+ _order.push(plugin);
20
+
21
+ // Map primary AST node type
22
+ if (plugin.astNodeType) {
23
+ _astTypeMap.set(plugin.astNodeType, plugin);
24
+ }
25
+
26
+ // Map child AST node types (for analyzer dispatch)
27
+ if (plugin.analyzer?.childNodeTypes) {
28
+ for (const t of plugin.analyzer.childNodeTypes) {
29
+ _astTypeMap.set(t, plugin);
30
+ }
31
+ }
32
+
33
+ // Register no-op leaf types as sentinel in the same map
34
+ if (plugin.analyzer?.noopNodeTypes) {
35
+ for (const t of plugin.analyzer.noopNodeTypes) {
36
+ _astTypeMap.set(t, _NOOP_SENTINEL);
37
+ }
38
+ }
39
+ },
40
+
41
+ get(name) {
42
+ return _plugins.get(name) || null;
43
+ },
44
+
45
+ getByAstType(type) {
46
+ return _astTypeMap.get(type) || null;
47
+ },
48
+
49
+ isNoopType(type) {
50
+ return _astTypeMap.get(type) === _NOOP_SENTINEL;
51
+ },
52
+
53
+ all() {
54
+ return _order; // callers must not mutate; treated as read-only
55
+ },
56
+ };
@@ -0,0 +1,23 @@
1
+ import { TokenType } from '../../lexer/tokens.js';
2
+
3
+ export const benchPlugin = {
4
+ name: 'bench',
5
+ astNodeType: 'BenchBlock',
6
+ detection: {
7
+ strategy: 'identifier',
8
+ identifierValue: 'bench',
9
+ lookahead: (parser) => {
10
+ const next = parser.peek(1);
11
+ return next.type === TokenType.LBRACE || next.type === TokenType.STRING;
12
+ },
13
+ },
14
+ parser: {
15
+ install: null,
16
+ installedFlag: null,
17
+ method: 'parseBenchBlock',
18
+ },
19
+ analyzer: {
20
+ visit: (analyzer, node) => analyzer.visitTestBlock(node), // reuses test visitor
21
+ },
22
+ codegen: {},
23
+ };
@@ -0,0 +1,30 @@
1
+ import { installBrowserParser } from '../../parser/browser-parser.js';
2
+ import { installBrowserAnalyzer } from '../../analyzer/browser-analyzer.js';
3
+
4
+ export const browserPlugin = {
5
+ name: 'browser',
6
+ astNodeType: 'BrowserBlock',
7
+ detection: {
8
+ strategy: 'keyword',
9
+ tokenType: 'BROWSER',
10
+ },
11
+ parser: {
12
+ install: installBrowserParser,
13
+ installedFlag: '_browserParserInstalled',
14
+ method: 'parseBrowserBlock',
15
+ },
16
+ analyzer: {
17
+ visit: (analyzer, node) => {
18
+ if (!analyzer.constructor.prototype._browserAnalyzerInstalled) {
19
+ installBrowserAnalyzer(analyzer.constructor);
20
+ }
21
+ const methodName = 'visit' + node.type;
22
+ return analyzer[methodName](node);
23
+ },
24
+ childNodeTypes: [
25
+ 'StateDeclaration', 'ComputedDeclaration', 'EffectDeclaration',
26
+ 'ComponentDeclaration', 'StoreDeclaration', 'FormDeclaration',
27
+ ],
28
+ },
29
+ codegen: {},
30
+ };
@@ -0,0 +1,24 @@
1
+ import { installCliParser } from '../../parser/cli-parser.js';
2
+
3
+ export const cliPlugin = {
4
+ name: 'cli',
5
+ astNodeType: 'CliBlock',
6
+ detection: {
7
+ strategy: 'identifier',
8
+ identifierValue: 'cli',
9
+ },
10
+ parser: {
11
+ install: installCliParser,
12
+ installedFlag: '_cliParserInstalled',
13
+ method: 'parseCliBlock',
14
+ },
15
+ analyzer: {
16
+ visit: (analyzer, node) => analyzer.visitCliBlock(node),
17
+ noopNodeTypes: ['CliConfigField', 'CliCommandDeclaration', 'CliParam'],
18
+ crossBlockValidate: (analyzer) => analyzer._validateCliCrossBlock(),
19
+ },
20
+ codegen: {
21
+ earlyReturn: true,
22
+ earlyReturnMethod: '_generateCli',
23
+ },
24
+ };
@@ -0,0 +1,21 @@
1
+ export const dataPlugin = {
2
+ name: 'data',
3
+ astNodeType: 'DataBlock',
4
+ detection: {
5
+ strategy: 'identifier',
6
+ identifierValue: 'data',
7
+ },
8
+ parser: {
9
+ install: null,
10
+ installedFlag: null,
11
+ method: 'parseDataBlock',
12
+ },
13
+ analyzer: {
14
+ visit: (analyzer, node) => analyzer.visitDataBlock(node),
15
+ noopNodeTypes: [
16
+ 'SourceDeclaration', 'PipelineDeclaration',
17
+ 'ValidateBlock', 'RefreshPolicy',
18
+ ],
19
+ },
20
+ codegen: {},
21
+ };
@@ -0,0 +1,32 @@
1
+ import { installEdgeParser } from '../../parser/edge-parser.js';
2
+ import { TokenType } from '../../lexer/tokens.js';
3
+
4
+ export const edgePlugin = {
5
+ name: 'edge',
6
+ astNodeType: 'EdgeBlock',
7
+ detection: {
8
+ strategy: 'identifier',
9
+ identifierValue: 'edge',
10
+ lookahead: (parser) => {
11
+ const next = parser.peek(1);
12
+ // edge {} or edge "name" {}
13
+ return next.type === TokenType.LBRACE || next.type === TokenType.STRING;
14
+ },
15
+ },
16
+ parser: {
17
+ install: installEdgeParser,
18
+ installedFlag: '_edgeParserInstalled',
19
+ method: 'parseEdgeBlock',
20
+ },
21
+ analyzer: {
22
+ visit: (analyzer, node) => analyzer.visitEdgeBlock(node),
23
+ childNodeTypes: [],
24
+ noopNodeTypes: [
25
+ 'EdgeKVDeclaration', 'EdgeSQLDeclaration', 'EdgeStorageDeclaration',
26
+ 'EdgeQueueDeclaration', 'EdgeEnvDeclaration', 'EdgeSecretDeclaration',
27
+ 'EdgeScheduleDeclaration', 'EdgeConsumeDeclaration', 'EdgeConfigField',
28
+ ],
29
+ crossBlockValidate: (analyzer) => analyzer._validateEdgeCrossBlock(),
30
+ },
31
+ codegen: {},
32
+ };
@@ -0,0 +1,27 @@
1
+ import { installSecurityParser } from '../../parser/security-parser.js';
2
+
3
+ export const securityPlugin = {
4
+ name: 'security',
5
+ astNodeType: 'SecurityBlock',
6
+ detection: {
7
+ strategy: 'identifier',
8
+ identifierValue: 'security',
9
+ },
10
+ parser: {
11
+ install: installSecurityParser,
12
+ installedFlag: '_securityParserInstalled',
13
+ method: 'parseSecurityBlock',
14
+ },
15
+ analyzer: {
16
+ visit: (analyzer, node) => analyzer.visitSecurityBlock(node),
17
+ noopNodeTypes: [
18
+ 'SecurityAuthDeclaration', 'SecurityRoleDeclaration',
19
+ 'SecurityProtectDeclaration', 'SecuritySensitiveDeclaration',
20
+ 'SecurityCorsDeclaration', 'SecurityCspDeclaration',
21
+ 'SecurityRateLimitDeclaration', 'SecurityCsrfDeclaration',
22
+ 'SecurityAuditDeclaration',
23
+ ],
24
+ crossBlockValidate: (analyzer) => analyzer._validateSecurityCrossBlock(),
25
+ },
26
+ codegen: {},
27
+ };
@@ -0,0 +1,46 @@
1
+ import { installServerParser } from '../../parser/server-parser.js';
2
+ import { collectServerBlockFunctions, installServerAnalyzer } from '../../analyzer/server-analyzer.js';
3
+
4
+ export const serverPlugin = {
5
+ name: 'server',
6
+ astNodeType: 'ServerBlock',
7
+ detection: {
8
+ strategy: 'keyword',
9
+ tokenType: 'SERVER',
10
+ },
11
+ parser: {
12
+ install: installServerParser,
13
+ installedFlag: '_serverParserInstalled',
14
+ method: 'parseServerBlock',
15
+ },
16
+ analyzer: {
17
+ visit: (analyzer, node) => {
18
+ if (!analyzer.constructor.prototype._serverAnalyzerInstalled) {
19
+ installServerAnalyzer(analyzer.constructor);
20
+ }
21
+ return analyzer._visitServerNode(node);
22
+ },
23
+ childNodeTypes: [
24
+ 'RouteDeclaration', 'MiddlewareDeclaration', 'HealthCheckDeclaration',
25
+ 'CorsDeclaration', 'ErrorHandlerDeclaration', 'WebSocketDeclaration',
26
+ 'StaticDeclaration', 'DiscoverDeclaration', 'AuthDeclaration',
27
+ 'MaxBodyDeclaration', 'RouteGroupDeclaration', 'RateLimitDeclaration',
28
+ 'LifecycleHookDeclaration', 'SubscribeDeclaration', 'EnvDeclaration',
29
+ 'ScheduleDeclaration', 'UploadDeclaration', 'SessionDeclaration',
30
+ 'DbDeclaration', 'TlsDeclaration', 'CompressionDeclaration',
31
+ 'BackgroundJobDeclaration', 'CacheDeclaration', 'SseDeclaration',
32
+ 'ModelDeclaration',
33
+ ],
34
+ noopNodeTypes: ['AiConfigDeclaration'],
35
+ prePass: (analyzer) => {
36
+ const has = analyzer.ast.body.some(n => n.type === 'ServerBlock');
37
+ if (has) {
38
+ installServerAnalyzer(analyzer.constructor);
39
+ analyzer.serverBlockFunctions = collectServerBlockFunctions(analyzer.ast);
40
+ } else {
41
+ analyzer.serverBlockFunctions = new Map();
42
+ }
43
+ },
44
+ },
45
+ codegen: {},
46
+ };
@@ -0,0 +1,17 @@
1
+ export const sharedPlugin = {
2
+ name: 'shared',
3
+ astNodeType: 'SharedBlock',
4
+ detection: {
5
+ strategy: 'keyword',
6
+ tokenType: 'SHARED',
7
+ },
8
+ parser: {
9
+ install: null,
10
+ installedFlag: null,
11
+ method: 'parseSharedBlock',
12
+ },
13
+ analyzer: {
14
+ visit: (analyzer, node) => analyzer.visitSharedBlock(node),
15
+ },
16
+ codegen: {},
17
+ };
@@ -0,0 +1,23 @@
1
+ import { TokenType } from '../../lexer/tokens.js';
2
+
3
+ export const testPlugin = {
4
+ name: 'test',
5
+ astNodeType: 'TestBlock',
6
+ detection: {
7
+ strategy: 'identifier',
8
+ identifierValue: 'test',
9
+ lookahead: (parser) => {
10
+ const next = parser.peek(1);
11
+ return next.type === TokenType.LBRACE || next.type === TokenType.STRING;
12
+ },
13
+ },
14
+ parser: {
15
+ install: null,
16
+ installedFlag: null,
17
+ method: 'parseTestBlock',
18
+ },
19
+ analyzer: {
20
+ visit: (analyzer, node) => analyzer.visitTestBlock(node),
21
+ },
22
+ codegen: {},
23
+ };
@@ -0,0 +1,25 @@
1
+ // Registers all built-in block plugins and re-exports BlockRegistry.
2
+ // Import this module (instead of block-registry.js directly) to ensure all plugins are loaded.
3
+
4
+ import { BlockRegistry } from './block-registry.js';
5
+ import { serverPlugin } from './plugins/server-plugin.js';
6
+ import { browserPlugin } from './plugins/browser-plugin.js';
7
+ import { sharedPlugin } from './plugins/shared-plugin.js';
8
+ import { securityPlugin } from './plugins/security-plugin.js';
9
+ import { cliPlugin } from './plugins/cli-plugin.js';
10
+ import { dataPlugin } from './plugins/data-plugin.js';
11
+ import { testPlugin } from './plugins/test-plugin.js';
12
+ import { benchPlugin } from './plugins/bench-plugin.js';
13
+ import { edgePlugin } from './plugins/edge-plugin.js';
14
+
15
+ BlockRegistry.register(serverPlugin);
16
+ BlockRegistry.register(browserPlugin);
17
+ BlockRegistry.register(sharedPlugin);
18
+ BlockRegistry.register(securityPlugin);
19
+ BlockRegistry.register(cliPlugin);
20
+ BlockRegistry.register(dataPlugin);
21
+ BlockRegistry.register(testPlugin);
22
+ BlockRegistry.register(benchPlugin);
23
+ BlockRegistry.register(edgePlugin);
24
+
25
+ export { BlockRegistry };
@@ -243,7 +243,7 @@ export function renderHeadTags(tags) {
243
243
  // of tag descriptors for safe rendering: [{ tag: 'meta', attrs: { name: 'desc', content: '...' } }]
244
244
  // SECURITY: Raw string `head` must contain only developer-authored content — never user input.
245
245
  // Use the array form or renderHeadTags() for safe user-controlled head content.
246
- export function renderPage(component, { title = 'Tova App', head = '', scriptSrc = '/client.js', cspNonce } = {}) {
246
+ export function renderPage(component, { title = 'Tova App', head = '', scriptSrc = '/browser.js', cspNonce } = {}) {
247
247
  const appHtml = renderToString(typeof component === 'function' ? component() : component);
248
248
  const headHtml = Array.isArray(head) ? renderHeadTags(head) : head;
249
249
  const nonceAttr = cspNonce ? ` nonce="${escapeAttr(cspNonce)}"` : '';
@@ -403,7 +403,7 @@ export function renderToReadableStream(vnode, options = {}) {
403
403
 
404
404
  // Render a full HTML page as a stream
405
405
  export function renderPageToStream(component, options = {}) {
406
- const { title = 'Tova App', head = '', scriptSrc = '/client.js', onError, bufferSize, cspNonce } = options;
406
+ const { title = 'Tova App', head = '', scriptSrc = '/browser.js', onError, bufferSize, cspNonce } = options;
407
407
  const headHtml = Array.isArray(head) ? renderHeadTags(head) : head;
408
408
  const nonceAttr = cspNonce ? ` nonce="${escapeAttr(cspNonce)}"` : '';
409
409