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
package/src/parser/parser.js
CHANGED
|
@@ -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 {
|
|
4
|
-
import {
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
267
|
-
if (node.body && Array.isArray(node.body))
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 };
|
package/src/runtime/ssr.js
CHANGED
|
@@ -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 = '/
|
|
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 = '/
|
|
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
|
|