tova 0.5.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tova.js +261 -60
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +351 -11
- package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/form-analyzer.js +113 -0
- package/src/analyzer/scope.js +2 -2
- package/src/codegen/base-codegen.js +1160 -10
- package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
- package/src/codegen/codegen.js +119 -28
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/edge-codegen.js +1351 -0
- package/src/codegen/form-codegen.js +553 -0
- package/src/codegen/security-codegen.js +5 -5
- package/src/codegen/server-codegen.js +88 -7
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/wasm-codegen.js +6 -0
- package/src/config/edit-toml.js +6 -2
- package/src/config/git-resolver.js +128 -0
- package/src/config/lock-file.js +57 -0
- package/src/config/module-cache.js +58 -0
- package/src/config/module-entry.js +37 -0
- package/src/config/module-path.js +31 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +17 -0
- package/src/config/resolver.js +139 -0
- package/src/config/search.js +28 -0
- package/src/config/semver.js +72 -0
- package/src/config/toml.js +48 -5
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +311 -0
- package/src/diagnostics/error-codes.js +1 -1
- package/src/docs/generator.js +1 -1
- package/src/formatter/formatter.js +4 -4
- package/src/lexer/tokens.js +12 -2
- package/src/lsp/server.js +483 -1
- package/src/parser/ast.js +60 -5
- package/src/parser/{client-ast.js → browser-ast.js} +3 -3
- package/src/parser/{client-parser.js → browser-parser.js} +42 -15
- package/src/parser/concurrency-ast.js +15 -0
- package/src/parser/concurrency-parser.js +236 -0
- package/src/parser/deploy-ast.js +37 -0
- package/src/parser/deploy-parser.js +132 -0
- package/src/parser/edge-ast.js +83 -0
- package/src/parser/edge-parser.js +262 -0
- package/src/parser/form-ast.js +80 -0
- package/src/parser/form-parser.js +206 -0
- package/src/parser/parser.js +82 -14
- package/src/parser/select-ast.js +39 -0
- package/src/registry/plugins/browser-plugin.js +30 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/edge-plugin.js +32 -0
- package/src/registry/register-all.js +8 -2
- package/src/runtime/ssr.js +2 -2
- package/src/stdlib/inline.js +38 -6
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/version.js +1 -1
- package/src/registry/plugins/client-plugin.js +0 -30
package/src/parser/parser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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 { FormValidator } from './form-ast.js';
|
|
3
4
|
import { BlockRegistry } from '../registry/register-all.js';
|
|
4
5
|
|
|
5
6
|
export class Parser {
|
|
@@ -75,6 +76,16 @@ export class Parser {
|
|
|
75
76
|
this.error(message || `Expected ${type}, got ${this.current().type}`);
|
|
76
77
|
}
|
|
77
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
|
+
|
|
78
89
|
loc() {
|
|
79
90
|
const tok = this.current();
|
|
80
91
|
return { line: tok.line, column: tok.column, file: this.filename };
|
|
@@ -95,7 +106,7 @@ export class Parser {
|
|
|
95
106
|
tok.type === TokenType.WHILE || tok.type === TokenType.RETURN ||
|
|
96
107
|
tok.type === TokenType.IMPORT || tok.type === TokenType.MATCH ||
|
|
97
108
|
tok.type === TokenType.TRY || tok.type === TokenType.SERVER ||
|
|
98
|
-
tok.type === TokenType.
|
|
109
|
+
tok.type === TokenType.BROWSER || tok.type === TokenType.SHARED ||
|
|
99
110
|
tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
|
|
100
111
|
tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
|
|
101
112
|
tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
|
|
@@ -120,13 +131,15 @@ export class Parser {
|
|
|
120
131
|
_isContextualKeyword() {
|
|
121
132
|
const t = this.current().type;
|
|
122
133
|
return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
|
|
123
|
-
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE
|
|
134
|
+
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE ||
|
|
135
|
+
t === TokenType.FORM || t === TokenType.FIELD || t === TokenType.GROUP || t === TokenType.STEPS;
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
_isContextualKeywordToken(token) {
|
|
127
139
|
const t = token.type;
|
|
128
140
|
return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
|
|
129
|
-
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE
|
|
141
|
+
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE ||
|
|
142
|
+
t === TokenType.FORM || t === TokenType.FIELD || t === TokenType.GROUP || t === TokenType.STEPS;
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
_synchronizeBlock() {
|
|
@@ -144,7 +157,7 @@ export class Parser {
|
|
|
144
157
|
tok.type === TokenType.WHILE || tok.type === TokenType.RETURN ||
|
|
145
158
|
tok.type === TokenType.IMPORT || tok.type === TokenType.MATCH ||
|
|
146
159
|
tok.type === TokenType.TRY || tok.type === TokenType.SERVER ||
|
|
147
|
-
tok.type === TokenType.
|
|
160
|
+
tok.type === TokenType.BROWSER || tok.type === TokenType.SHARED ||
|
|
148
161
|
tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
|
|
149
162
|
tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
|
|
150
163
|
tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
|
|
@@ -163,7 +176,8 @@ export class Parser {
|
|
|
163
176
|
const next = this.peek(1);
|
|
164
177
|
// Fragment: <>
|
|
165
178
|
if (next.type === TokenType.GREATER) return true;
|
|
166
|
-
|
|
179
|
+
// Accept identifiers and keywords as JSX tag names (e.g., <form>, <label>, <field>)
|
|
180
|
+
if (next.type !== TokenType.IDENTIFIER && !(next.value in Keywords)) return false;
|
|
167
181
|
// Uppercase tag is always a component reference, never a comparison variable
|
|
168
182
|
if (/^[A-Z]/.test(next.value)) return true;
|
|
169
183
|
const afterIdent = this.peek(2);
|
|
@@ -198,6 +212,13 @@ export class Parser {
|
|
|
198
212
|
// ─── Program ───────────────────────────────────────────────
|
|
199
213
|
|
|
200
214
|
parse() {
|
|
215
|
+
// Eagerly install all block-plugin parser extensions so they work inside function bodies
|
|
216
|
+
for (const plugin of BlockRegistry.all()) {
|
|
217
|
+
const p = plugin.parser;
|
|
218
|
+
if (p.install && p.installedFlag && !Parser.prototype[p.installedFlag]) {
|
|
219
|
+
p.install(Parser);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
201
222
|
const body = [];
|
|
202
223
|
const maxErrors = 50; // Stop after 50 errors to avoid cascading noise
|
|
203
224
|
while (!this.isAtEnd()) {
|
|
@@ -378,7 +399,7 @@ export class Parser {
|
|
|
378
399
|
}
|
|
379
400
|
|
|
380
401
|
// ─── Full-stack blocks ────────────────────────────────────
|
|
381
|
-
//
|
|
402
|
+
// parseBrowserBlock() and browser-specific methods are in browser-parser.js (lazy-loaded)
|
|
382
403
|
|
|
383
404
|
parseSharedBlock() {
|
|
384
405
|
const l = this.loc();
|
|
@@ -521,7 +542,7 @@ export class Parser {
|
|
|
521
542
|
return new AST.RefreshPolicy(sourceName, { value, unit }, l);
|
|
522
543
|
}
|
|
523
544
|
|
|
524
|
-
//
|
|
545
|
+
// Browser-specific statements and JSX parsing are in browser-parser.js (lazy-loaded)
|
|
525
546
|
|
|
526
547
|
// ─── Statements ───────────────────────────────────────────
|
|
527
548
|
|
|
@@ -980,6 +1001,43 @@ export class Parser {
|
|
|
980
1001
|
return new AST.TypeAnnotation(name, typeParams, l);
|
|
981
1002
|
}
|
|
982
1003
|
|
|
1004
|
+
// Parse inline validators for type fields: { required, email, min(18) }
|
|
1005
|
+
// Uses comma-separated validator names, supports args in parens
|
|
1006
|
+
_parseTypeFieldValidators() {
|
|
1007
|
+
const validators = [];
|
|
1008
|
+
if (this.check(TokenType.LBRACE)) {
|
|
1009
|
+
this.advance(); // consume {
|
|
1010
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
1011
|
+
validators.push(this._parseInlineValidator());
|
|
1012
|
+
this.match(TokenType.COMMA); // optional comma separator
|
|
1013
|
+
}
|
|
1014
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close validator block");
|
|
1015
|
+
}
|
|
1016
|
+
return validators;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Parse a single inline validator: name or name(args...)
|
|
1020
|
+
_parseInlineValidator() {
|
|
1021
|
+
const l = this.loc();
|
|
1022
|
+
let isAsync = false;
|
|
1023
|
+
if (this.check(TokenType.ASYNC)) {
|
|
1024
|
+
isAsync = true;
|
|
1025
|
+
this.advance();
|
|
1026
|
+
}
|
|
1027
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected validator name").value;
|
|
1028
|
+
const args = [];
|
|
1029
|
+
if (this.match(TokenType.LPAREN)) {
|
|
1030
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
1031
|
+
args.push(this.parseExpression());
|
|
1032
|
+
while (this.match(TokenType.COMMA)) {
|
|
1033
|
+
args.push(this.parseExpression());
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
this.expect(TokenType.RPAREN, "Expected ')' after validator arguments");
|
|
1037
|
+
}
|
|
1038
|
+
return new FormValidator(name, args, isAsync, l);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
983
1041
|
parseTypeDeclaration() {
|
|
984
1042
|
const l = this.loc();
|
|
985
1043
|
this.expect(TokenType.TYPE);
|
|
@@ -1066,9 +1124,10 @@ export class Parser {
|
|
|
1066
1124
|
this.expect(TokenType.RPAREN, "Expected ')' after variant fields");
|
|
1067
1125
|
variants.push(new AST.TypeVariant(vname, fields, vl));
|
|
1068
1126
|
} else if (this.match(TokenType.COLON)) {
|
|
1069
|
-
// Simple field: name: String
|
|
1127
|
+
// Simple field: name: String or name: String { required, email }
|
|
1070
1128
|
const ftype = this.parseTypeAnnotation();
|
|
1071
|
-
|
|
1129
|
+
const validators = this._parseTypeFieldValidators();
|
|
1130
|
+
variants.push(new AST.TypeField(vname, ftype, vl, validators));
|
|
1072
1131
|
} else {
|
|
1073
1132
|
// Bare variant: None
|
|
1074
1133
|
variants.push(new AST.TypeVariant(vname, [], vl));
|
|
@@ -1168,7 +1227,12 @@ export class Parser {
|
|
|
1168
1227
|
const elements = [];
|
|
1169
1228
|
|
|
1170
1229
|
while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
|
|
1171
|
-
if (this.check(TokenType.
|
|
1230
|
+
if (this.check(TokenType.SPREAD)) {
|
|
1231
|
+
this.advance(); // consume ...
|
|
1232
|
+
const restName = this.expect(TokenType.IDENTIFIER, "Expected identifier after '...'").value;
|
|
1233
|
+
elements.push('...' + restName);
|
|
1234
|
+
break; // rest must be last
|
|
1235
|
+
} else if (this.check(TokenType.IDENTIFIER) && this.current().value === '_') {
|
|
1172
1236
|
elements.push(null); // skip placeholder
|
|
1173
1237
|
this.advance();
|
|
1174
1238
|
} else {
|
|
@@ -1744,7 +1808,7 @@ export class Parser {
|
|
|
1744
1808
|
expr = new AST.MemberExpression(expr, new AST.NumberLiteral(idx, l), true, l);
|
|
1745
1809
|
continue;
|
|
1746
1810
|
}
|
|
1747
|
-
const prop = this.
|
|
1811
|
+
const prop = this.expectPropertyName("Expected property name after '.'").value;
|
|
1748
1812
|
expr = new AST.MemberExpression(expr, prop, false, l);
|
|
1749
1813
|
continue;
|
|
1750
1814
|
}
|
|
@@ -1752,7 +1816,7 @@ export class Parser {
|
|
|
1752
1816
|
if (this.check(TokenType.QUESTION_DOT)) {
|
|
1753
1817
|
const l = this.loc();
|
|
1754
1818
|
this.advance();
|
|
1755
|
-
const prop = this.
|
|
1819
|
+
const prop = this.expectPropertyName("Expected property name after '?.'").value;
|
|
1756
1820
|
expr = new AST.OptionalChain(expr, prop, false, l);
|
|
1757
1821
|
continue;
|
|
1758
1822
|
}
|
|
@@ -1950,9 +2014,13 @@ export class Parser {
|
|
|
1950
2014
|
return this.parseParenOrArrowLambda();
|
|
1951
2015
|
|
|
1952
2016
|
case TokenType.SERVER:
|
|
1953
|
-
case TokenType.
|
|
2017
|
+
case TokenType.BROWSER:
|
|
1954
2018
|
case TokenType.SHARED:
|
|
1955
2019
|
case TokenType.DERIVE:
|
|
2020
|
+
case TokenType.FORM:
|
|
2021
|
+
case TokenType.FIELD:
|
|
2022
|
+
case TokenType.GROUP:
|
|
2023
|
+
case TokenType.STEPS:
|
|
1956
2024
|
return new AST.Identifier(this.advance().value, l);
|
|
1957
2025
|
|
|
1958
2026
|
case TokenType.IDENTIFIER: {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Select-specific AST Node definitions for the Tova language
|
|
2
|
+
// Extracted for lazy loading -- only loaded when select { } blocks are used.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* select {
|
|
6
|
+
* msg from ch => { ... }
|
|
7
|
+
* ch.send(val) => { ... }
|
|
8
|
+
* timeout(5000) => { ... }
|
|
9
|
+
* _ => { ... }
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export class SelectStatement {
|
|
13
|
+
constructor(cases, loc) {
|
|
14
|
+
this.type = 'SelectStatement';
|
|
15
|
+
this.cases = cases; // Array of SelectCase
|
|
16
|
+
this.loc = loc;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A single case arm inside a select block.
|
|
22
|
+
*
|
|
23
|
+
* kind: "receive" | "send" | "timeout" | "default"
|
|
24
|
+
* channel: Expression | null (identifier for the channel; null for timeout/default)
|
|
25
|
+
* binding: string | null (variable name bound on receive; null otherwise)
|
|
26
|
+
* value: Expression | null (value to send, or timeout duration; null for receive/default)
|
|
27
|
+
* body: [Statement] (statements executed when this case fires)
|
|
28
|
+
*/
|
|
29
|
+
export class SelectCase {
|
|
30
|
+
constructor(kind, channel, binding, value, body, loc) {
|
|
31
|
+
this.type = 'SelectCase';
|
|
32
|
+
this.kind = kind; // "receive" | "send" | "timeout" | "default"
|
|
33
|
+
this.channel = channel; // Expression | null
|
|
34
|
+
this.binding = binding; // string | null
|
|
35
|
+
this.value = value; // Expression | null
|
|
36
|
+
this.body = body; // [Statement]
|
|
37
|
+
this.loc = loc;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -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,32 @@
|
|
|
1
|
+
import { TokenType } from '../../lexer/tokens.js';
|
|
2
|
+
import { installConcurrencyParser } from '../../parser/concurrency-parser.js';
|
|
3
|
+
|
|
4
|
+
export const concurrencyPlugin = {
|
|
5
|
+
name: 'concurrency',
|
|
6
|
+
astNodeType: 'ConcurrentBlock',
|
|
7
|
+
detection: {
|
|
8
|
+
strategy: 'identifier',
|
|
9
|
+
identifierValue: 'concurrent',
|
|
10
|
+
lookahead: (parser) => {
|
|
11
|
+
const next = parser.peek(1);
|
|
12
|
+
// concurrent {} or concurrent mode {}
|
|
13
|
+
return next.type === TokenType.LBRACE ||
|
|
14
|
+
(next.type === TokenType.IDENTIFIER &&
|
|
15
|
+
['cancel_on_error', 'first', 'timeout'].includes(next.value));
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
parser: {
|
|
19
|
+
install: installConcurrencyParser,
|
|
20
|
+
installedFlag: '_concurrencyParserInstalled',
|
|
21
|
+
method: 'parseConcurrentBlock',
|
|
22
|
+
},
|
|
23
|
+
analyzer: {
|
|
24
|
+
visit: (analyzer, node) => {
|
|
25
|
+
if (node.type === 'SelectStatement') return analyzer.visitSelectStatement(node);
|
|
26
|
+
return analyzer.visitConcurrentBlock(node);
|
|
27
|
+
},
|
|
28
|
+
childNodeTypes: ['SelectStatement'],
|
|
29
|
+
noopNodeTypes: ['SpawnExpression', 'SelectCase'],
|
|
30
|
+
},
|
|
31
|
+
codegen: {},
|
|
32
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { installDeployParser } from '../../parser/deploy-parser.js';
|
|
2
|
+
import { installDeployAnalyzer } from '../../analyzer/deploy-analyzer.js';
|
|
3
|
+
import { TokenType } from '../../lexer/tokens.js';
|
|
4
|
+
|
|
5
|
+
export const deployPlugin = {
|
|
6
|
+
name: 'deploy',
|
|
7
|
+
astNodeType: 'DeployBlock',
|
|
8
|
+
detection: {
|
|
9
|
+
strategy: 'identifier',
|
|
10
|
+
identifierValue: 'deploy',
|
|
11
|
+
lookahead: (parser) => {
|
|
12
|
+
const next = parser.peek(1);
|
|
13
|
+
// deploy "name" {} — name is required
|
|
14
|
+
return next.type === TokenType.STRING;
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
parser: {
|
|
18
|
+
install: installDeployParser,
|
|
19
|
+
installedFlag: '_deployParserInstalled',
|
|
20
|
+
method: 'parseDeployBlock',
|
|
21
|
+
},
|
|
22
|
+
analyzer: {
|
|
23
|
+
visit: (analyzer, node) => {
|
|
24
|
+
installDeployAnalyzer(analyzer.constructor);
|
|
25
|
+
analyzer.visitDeployBlock(node);
|
|
26
|
+
},
|
|
27
|
+
childNodeTypes: [],
|
|
28
|
+
noopNodeTypes: [
|
|
29
|
+
'DeployConfigField', 'DeployEnvBlock', 'DeployDbBlock',
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
codegen: {},
|
|
33
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -3,21 +3,27 @@
|
|
|
3
3
|
|
|
4
4
|
import { BlockRegistry } from './block-registry.js';
|
|
5
5
|
import { serverPlugin } from './plugins/server-plugin.js';
|
|
6
|
-
import {
|
|
6
|
+
import { browserPlugin } from './plugins/browser-plugin.js';
|
|
7
7
|
import { sharedPlugin } from './plugins/shared-plugin.js';
|
|
8
8
|
import { securityPlugin } from './plugins/security-plugin.js';
|
|
9
9
|
import { cliPlugin } from './plugins/cli-plugin.js';
|
|
10
10
|
import { dataPlugin } from './plugins/data-plugin.js';
|
|
11
11
|
import { testPlugin } from './plugins/test-plugin.js';
|
|
12
12
|
import { benchPlugin } from './plugins/bench-plugin.js';
|
|
13
|
+
import { edgePlugin } from './plugins/edge-plugin.js';
|
|
14
|
+
import { concurrencyPlugin } from './plugins/concurrency-plugin.js';
|
|
15
|
+
import { deployPlugin } from './plugins/deploy-plugin.js';
|
|
13
16
|
|
|
14
17
|
BlockRegistry.register(serverPlugin);
|
|
15
|
-
BlockRegistry.register(
|
|
18
|
+
BlockRegistry.register(browserPlugin);
|
|
16
19
|
BlockRegistry.register(sharedPlugin);
|
|
17
20
|
BlockRegistry.register(securityPlugin);
|
|
18
21
|
BlockRegistry.register(cliPlugin);
|
|
19
22
|
BlockRegistry.register(dataPlugin);
|
|
20
23
|
BlockRegistry.register(testPlugin);
|
|
21
24
|
BlockRegistry.register(benchPlugin);
|
|
25
|
+
BlockRegistry.register(edgePlugin);
|
|
26
|
+
BlockRegistry.register(concurrencyPlugin);
|
|
27
|
+
BlockRegistry.register(deployPlugin);
|
|
22
28
|
|
|
23
29
|
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
|
|
package/src/stdlib/inline.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Tova standard library — inline string versions for codegen
|
|
2
2
|
// Single source of truth for all inline stdlib code used in code generation.
|
|
3
|
-
// Used by: base-codegen.js,
|
|
3
|
+
// Used by: base-codegen.js, browser-codegen.js, bin/tova.js
|
|
4
4
|
|
|
5
5
|
export const RESULT_OPTION = `class _Ok { constructor(value) { this.value = value; } }
|
|
6
6
|
_Ok.prototype.__tag = "Ok";
|
|
@@ -117,7 +117,7 @@ export const BUILTIN_FUNCTIONS = {
|
|
|
117
117
|
if (!parallel_map._pool) {
|
|
118
118
|
const { Worker } = await import("worker_threads");
|
|
119
119
|
const wc = 'const{parentPort}=require("worker_threads");parentPort.on("message",m=>{const fn=(0,eval)("("+m.f+")");try{const r=m.c.map(fn);parentPort.postMessage({i:m.i,r})}catch(e){parentPort.postMessage({i:m.i,e:e.message})}})';
|
|
120
|
-
parallel_map._pool = Array.from({length: n}, () =>
|
|
120
|
+
parallel_map._pool = Array.from({length: n}, () => new Worker(wc, {eval: true}));
|
|
121
121
|
parallel_map._cid = 0;
|
|
122
122
|
}
|
|
123
123
|
const pool = parallel_map._pool;
|
|
@@ -125,17 +125,20 @@ export const BUILTIN_FUNCTIONS = {
|
|
|
125
125
|
const fnStr = fn.toString();
|
|
126
126
|
const cid = ++parallel_map._cid;
|
|
127
127
|
const promises = [];
|
|
128
|
+
const usedWorkers = [];
|
|
128
129
|
for (let ci = 0; ci < pool.length && ci * cs < arr.length; ci++) {
|
|
129
130
|
const chunk = arr.slice(ci * cs, (ci + 1) * cs);
|
|
130
131
|
const mid = cid * 1000 + ci;
|
|
132
|
+
const w = pool[ci];
|
|
133
|
+
w.ref();
|
|
134
|
+
usedWorkers.push(w);
|
|
131
135
|
promises.push(new Promise((resolve, reject) => {
|
|
132
|
-
const w = pool[ci];
|
|
133
136
|
const h = (msg) => { if (msg.i === mid) { w.removeListener("message", h); if (msg.e) reject(new Error(msg.e)); else resolve(msg.r); } };
|
|
134
137
|
w.on("message", h);
|
|
135
138
|
w.postMessage({i: mid, c: chunk, f: fnStr});
|
|
136
139
|
}));
|
|
137
140
|
}
|
|
138
|
-
return (await Promise.all(promises)).flat();
|
|
141
|
+
try { return (await Promise.all(promises)).flat(); } finally { for (const w of usedWorkers) w.unref(); }
|
|
139
142
|
}`,
|
|
140
143
|
upper: `function upper(s) { return s.toUpperCase(); }`,
|
|
141
144
|
lower: `function lower(s) { return s.toLowerCase(); }`,
|
|
@@ -1054,6 +1057,35 @@ Table.prototype = { get rows() { return this._rows.length; }, get columns() { re
|
|
|
1054
1057
|
this._recvWaiters.push(resolve);
|
|
1055
1058
|
}.bind(this));
|
|
1056
1059
|
}
|
|
1060
|
+
_tryReceive() {
|
|
1061
|
+
if (this._buffer.length > 0) {
|
|
1062
|
+
const value = this._buffer.shift();
|
|
1063
|
+
if (this._sendWaiters.length > 0) {
|
|
1064
|
+
const waiter = this._sendWaiters.shift();
|
|
1065
|
+
this._buffer.push(waiter.value);
|
|
1066
|
+
waiter.resolve();
|
|
1067
|
+
}
|
|
1068
|
+
return Some(value);
|
|
1069
|
+
}
|
|
1070
|
+
if (this._sendWaiters.length > 0) {
|
|
1071
|
+
const waiter = this._sendWaiters.shift();
|
|
1072
|
+
waiter.resolve();
|
|
1073
|
+
return Some(waiter.value);
|
|
1074
|
+
}
|
|
1075
|
+
return None;
|
|
1076
|
+
}
|
|
1077
|
+
_trySend(value) {
|
|
1078
|
+
if (this._recvWaiters.length > 0) {
|
|
1079
|
+
const waiter = this._recvWaiters.shift();
|
|
1080
|
+
waiter(Some(value));
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
if (this._capacity > 0 && this._buffer.length < this._capacity) {
|
|
1084
|
+
this._buffer.push(value);
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1057
1089
|
close() {
|
|
1058
1090
|
this._closed = true;
|
|
1059
1091
|
for (const waiter of this._recvWaiters) waiter(None);
|
|
@@ -1461,7 +1493,7 @@ export function getFullStdlib() {
|
|
|
1461
1493
|
return `${NATIVE_INIT_SYNC}\n${buildSelectiveStdlib(BUILTIN_NAMES)}\n${RESULT_OPTION}\n${PROPAGATE}`;
|
|
1462
1494
|
}
|
|
1463
1495
|
|
|
1464
|
-
// Stdlib for
|
|
1465
|
-
export function
|
|
1496
|
+
// Stdlib for browser codegen (includes builtins + result/option + propagate)
|
|
1497
|
+
export function getBrowserStdlib() {
|
|
1466
1498
|
return `${buildSelectiveStdlib(BUILTIN_NAMES)}\n${RESULT_OPTION}\n${PROPAGATE}`;
|
|
1467
1499
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Tova Runtime Bridge
|
|
2
|
+
// Auto-discovers the native tova_runtime addon (napi-rs) and exposes a clean API.
|
|
3
|
+
// Falls back gracefully when the addon is not available.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
let _runtime = null; // null = not yet attempted, false = failed, object = loaded
|
|
8
|
+
let _available = false;
|
|
9
|
+
|
|
10
|
+
function _findAndLoad() {
|
|
11
|
+
const { existsSync, readdirSync } = require('fs');
|
|
12
|
+
const { join, dirname } = require('path');
|
|
13
|
+
|
|
14
|
+
const searchDirs = [
|
|
15
|
+
// Relative to this file (src/stdlib/) -> project root tova_runtime/
|
|
16
|
+
join(dirname(__filename), '..', '..', 'tova_runtime'),
|
|
17
|
+
join(dirname(__filename), '..', '..', 'tova_runtime', 'target', 'release'),
|
|
18
|
+
// System-wide install
|
|
19
|
+
join(process.env.HOME || '', '.tova', 'lib'),
|
|
20
|
+
// Next to the running script
|
|
21
|
+
dirname(process.argv[1] || ''),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
for (const dir of searchDirs) {
|
|
25
|
+
if (!existsSync(dir)) continue;
|
|
26
|
+
|
|
27
|
+
let entries;
|
|
28
|
+
try { entries = readdirSync(dir); } catch (_) { continue; }
|
|
29
|
+
|
|
30
|
+
// Search .node files first (napi-rs convention)
|
|
31
|
+
const nodeFiles = entries.filter(f => f.endsWith('.node'));
|
|
32
|
+
for (const f of nodeFiles) {
|
|
33
|
+
try { return require(join(dir, f)); } catch (_) { continue; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fall back to .dylib files (macOS raw cargo output)
|
|
37
|
+
const dylibFiles = entries.filter(f => f.endsWith('.dylib') && f.startsWith('libtova_runtime'));
|
|
38
|
+
for (const f of dylibFiles) {
|
|
39
|
+
try { return require(join(dir, f)); } catch (_) { continue; }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _init() {
|
|
47
|
+
if (_runtime !== null) return _available;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const loaded = _findAndLoad();
|
|
51
|
+
if (loaded && typeof loaded.healthCheck === 'function') {
|
|
52
|
+
_runtime = loaded;
|
|
53
|
+
_available = true;
|
|
54
|
+
} else {
|
|
55
|
+
_runtime = false;
|
|
56
|
+
_available = false;
|
|
57
|
+
}
|
|
58
|
+
} catch (_) {
|
|
59
|
+
_runtime = false;
|
|
60
|
+
_available = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return _available;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Public API ---
|
|
67
|
+
|
|
68
|
+
function isRuntimeAvailable() {
|
|
69
|
+
return _init();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function healthCheck() {
|
|
73
|
+
if (!_init()) return null;
|
|
74
|
+
return _runtime.healthCheck();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function channelCreate(capacity) {
|
|
78
|
+
if (!_init()) throw new Error('tova_runtime not available');
|
|
79
|
+
return _runtime.channelCreate(capacity);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function channelSend(id, value) {
|
|
83
|
+
if (!_init()) throw new Error('tova_runtime not available');
|
|
84
|
+
return _runtime.channelSend(id, value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function channelReceive(id) {
|
|
88
|
+
if (!_init()) throw new Error('tova_runtime not available');
|
|
89
|
+
return _runtime.channelReceive(id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function channelClose(id) {
|
|
93
|
+
if (!_init()) throw new Error('tova_runtime not available');
|
|
94
|
+
_runtime.channelClose(id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function execWasm(bytes, func, args) {
|
|
98
|
+
if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
|
|
99
|
+
return _runtime.execWasm(bytes, func, args);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function execWasmWithChannels(bytes, func, args) {
|
|
103
|
+
if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
|
|
104
|
+
return _runtime.execWasmWithChannels(bytes, func, args);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function concurrentWasm(tasks) {
|
|
108
|
+
if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
|
|
109
|
+
return _runtime.concurrentWasm(tasks);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function concurrentWasmWithChannels(tasks) {
|
|
113
|
+
if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
|
|
114
|
+
return _runtime.concurrentWasmWithChannels(tasks);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function concurrentWasmShared(tasks) {
|
|
118
|
+
if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
|
|
119
|
+
return _runtime.concurrentWasmShared(tasks);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function concurrentWasmFirst(tasks) {
|
|
123
|
+
if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
|
|
124
|
+
return _runtime.concurrentWasmFirst(tasks);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function concurrentWasmTimeout(tasks, timeoutMs) {
|
|
128
|
+
if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
|
|
129
|
+
return _runtime.concurrentWasmTimeout(tasks, timeoutMs);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function concurrentWasmCancelOnError(tasks) {
|
|
133
|
+
if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
|
|
134
|
+
return _runtime.concurrentWasmCancelOnError(tasks);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
isRuntimeAvailable,
|
|
139
|
+
healthCheck,
|
|
140
|
+
channelCreate,
|
|
141
|
+
channelSend,
|
|
142
|
+
channelReceive,
|
|
143
|
+
channelClose,
|
|
144
|
+
execWasm,
|
|
145
|
+
execWasmWithChannels,
|
|
146
|
+
concurrentWasm,
|
|
147
|
+
concurrentWasmWithChannels,
|
|
148
|
+
concurrentWasmShared,
|
|
149
|
+
concurrentWasmFirst,
|
|
150
|
+
concurrentWasmTimeout,
|
|
151
|
+
concurrentWasmCancelOnError,
|
|
152
|
+
};
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-runtime.js — do not edit
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.8.2";
|