tova 0.7.0 → 0.9.4
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 +1312 -139
- package/package.json +8 -1
- package/src/analyzer/analyzer.js +539 -11
- package/src/analyzer/browser-analyzer.js +56 -8
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/scope.js +7 -0
- package/src/analyzer/server-analyzer.js +33 -1
- package/src/codegen/base-codegen.js +1296 -23
- package/src/codegen/browser-codegen.js +725 -20
- package/src/codegen/codegen.js +87 -5
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/server-codegen.js +54 -6
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/theme-codegen.js +69 -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 +63 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +26 -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 +61 -6
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +315 -0
- package/src/diagnostics/security-scorecard.js +111 -0
- package/src/lexer/lexer.js +18 -3
- package/src/lsp/server.js +482 -0
- package/src/parser/animate-ast.js +45 -0
- package/src/parser/ast.js +39 -0
- package/src/parser/browser-ast.js +19 -1
- package/src/parser/browser-parser.js +221 -4
- 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/parser.js +42 -5
- package/src/parser/select-ast.js +39 -0
- package/src/parser/theme-ast.js +29 -0
- package/src/parser/theme-parser.js +70 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/theme-plugin.js +20 -0
- package/src/registry/register-all.js +6 -0
- package/src/runtime/charts.js +547 -0
- package/src/runtime/embedded.js +6 -2
- package/src/runtime/reactivity.js +60 -0
- package/src/runtime/router.js +703 -295
- package/src/runtime/table.js +606 -33
- package/src/stdlib/inline.js +365 -10
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/stdlib/string.js +84 -2
- package/src/stdlib/validation.js +1 -1
- package/src/version.js +1 -1
package/src/codegen/codegen.js
CHANGED
|
@@ -36,6 +36,18 @@ function getEdgeCodegen() {
|
|
|
36
36
|
return _EdgeCodegen;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
let _DeployCodegen;
|
|
40
|
+
function getDeployCodegen() {
|
|
41
|
+
if (!_DeployCodegen) _DeployCodegen = _require('./deploy-codegen.js').DeployCodegen;
|
|
42
|
+
return _DeployCodegen;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let _ThemeCodegen;
|
|
46
|
+
function getThemeCodegen() {
|
|
47
|
+
if (!_ThemeCodegen) _ThemeCodegen = _require('./theme-codegen.js').ThemeCodegen;
|
|
48
|
+
return _ThemeCodegen;
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
export class CodeGenerator {
|
|
40
52
|
constructor(ast, filename = '<stdin>', options = {}) {
|
|
41
53
|
this.ast = ast;
|
|
@@ -60,6 +72,11 @@ export class CodeGenerator {
|
|
|
60
72
|
const topLevel = [];
|
|
61
73
|
|
|
62
74
|
for (const node of this.ast.body) {
|
|
75
|
+
// pub component declarations at top level are module-level exports, not browser block children
|
|
76
|
+
if (node.type === 'ComponentDeclaration' && node.isPublic) {
|
|
77
|
+
topLevel.push(node);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
63
80
|
const plugin = BlockRegistry.getByAstType(node.type);
|
|
64
81
|
if (plugin) {
|
|
65
82
|
if (!blocksByType.has(plugin.name)) blocksByType.set(plugin.name, []);
|
|
@@ -87,20 +104,67 @@ export class CodeGenerator {
|
|
|
87
104
|
const dataBlocks = getBlocks('data');
|
|
88
105
|
const securityBlocks = getBlocks('security');
|
|
89
106
|
const edgeBlocks = getBlocks('edge');
|
|
107
|
+
const deployBlocks = getBlocks('deploy');
|
|
108
|
+
const themeBlocks = getBlocks('theme');
|
|
90
109
|
|
|
91
110
|
// Detect module mode: no blocks, only top-level statements
|
|
92
111
|
const hasAnyBlocks = BlockRegistry.all().some(p => getBlocks(p.name).length > 0);
|
|
93
112
|
const isModule = !hasAnyBlocks && topLevel.length > 0;
|
|
94
113
|
|
|
95
114
|
if (isModule) {
|
|
115
|
+
// Separate pub component declarations from other top-level nodes
|
|
116
|
+
const componentNodes = topLevel.filter(n => n.type === 'ComponentDeclaration');
|
|
117
|
+
const otherNodes = topLevel.filter(n => n.type !== 'ComponentDeclaration');
|
|
118
|
+
|
|
96
119
|
const moduleGen = new SharedCodegen();
|
|
97
120
|
moduleGen._sourceMapsEnabled = this._sourceMaps;
|
|
98
121
|
moduleGen.setSourceFile(this.filename);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
122
|
+
|
|
123
|
+
// Generate non-component top-level code
|
|
124
|
+
let moduleCode = '';
|
|
125
|
+
if (otherNodes.length > 0) {
|
|
126
|
+
const fakeBlock = { type: 'BlockStatement', body: otherNodes };
|
|
127
|
+
moduleCode = moduleGen.genBlockStatements(fakeBlock);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Generate pub component code using BrowserCodegen
|
|
131
|
+
let componentCode = '';
|
|
132
|
+
if (componentNodes.length > 0) {
|
|
133
|
+
const BrowserCodegen = getBrowserCodegen();
|
|
134
|
+
const compGen = new BrowserCodegen();
|
|
135
|
+
compGen._sourceMapsEnabled = this._sourceMaps;
|
|
136
|
+
compGen.setSourceFile(this.filename);
|
|
137
|
+
|
|
138
|
+
// Sort: parent components first, then compound (child) components
|
|
139
|
+
const sortedComponents = [...componentNodes].sort((a, b) => {
|
|
140
|
+
const aIsChild = a.parent ? 1 : 0;
|
|
141
|
+
const bIsChild = b.parent ? 1 : 0;
|
|
142
|
+
return aIsChild - bIsChild;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const compParts = [];
|
|
146
|
+
for (const comp of sortedComponents) {
|
|
147
|
+
if (comp.parent) {
|
|
148
|
+
// Compound component — assign as property on parent
|
|
149
|
+
compParts.push(`${comp.parent}.${comp.child} = ${compGen.generateComponent(comp)};`);
|
|
150
|
+
} else {
|
|
151
|
+
const exportPrefix = comp.isPublic ? 'export ' : '';
|
|
152
|
+
compParts.push(exportPrefix + compGen.generateComponent(comp));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
componentCode = compParts.join('\n\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
102
158
|
const helpers = moduleGen.generateHelpers();
|
|
103
|
-
const
|
|
159
|
+
const parts = [helpers];
|
|
160
|
+
|
|
161
|
+
// Add runtime imports for component DOM/reactivity functions
|
|
162
|
+
if (componentNodes.length > 0) {
|
|
163
|
+
parts.unshift(`import { createSignal, createEffect, createComputed, batch, onMount, onUnmount, onCleanup, tova_el, tova_fragment, tova_keyed, tova_inject_css, createRef, createContext, provide, inject } from './runtime/reactivity.js';`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
parts.push(moduleCode, componentCode);
|
|
167
|
+
const combined = parts.filter(s => s.trim()).join('\n').trim();
|
|
104
168
|
return {
|
|
105
169
|
shared: combined,
|
|
106
170
|
server: '',
|
|
@@ -164,6 +228,11 @@ export class CodeGenerator {
|
|
|
164
228
|
? getSecurityCodegen().mergeSecurityBlocks(securityBlocks)
|
|
165
229
|
: null;
|
|
166
230
|
|
|
231
|
+
// Merge theme blocks into a single config
|
|
232
|
+
const themeConfig = themeBlocks.length > 0
|
|
233
|
+
? getThemeCodegen().mergeThemeBlocks(themeBlocks)
|
|
234
|
+
: null;
|
|
235
|
+
|
|
167
236
|
// Generate server outputs (one per named group)
|
|
168
237
|
const servers = {};
|
|
169
238
|
for (const [name, blocks] of serverGroups) {
|
|
@@ -215,7 +284,7 @@ export class CodeGenerator {
|
|
|
215
284
|
const gen = new (getBrowserCodegen())();
|
|
216
285
|
gen._sourceMapsEnabled = this._sourceMaps;
|
|
217
286
|
const key = name || 'default';
|
|
218
|
-
browsers[key] = gen.generate(blocks, combinedShared, sharedGen._usedBuiltins, securityConfig, typeValidatorsMap);
|
|
287
|
+
browsers[key] = gen.generate(blocks, combinedShared, sharedGen._usedBuiltins, securityConfig, typeValidatorsMap, themeConfig);
|
|
219
288
|
}
|
|
220
289
|
|
|
221
290
|
// Generate edge outputs (one per named group)
|
|
@@ -232,6 +301,17 @@ export class CodeGenerator {
|
|
|
232
301
|
}
|
|
233
302
|
}
|
|
234
303
|
|
|
304
|
+
// Generate deploy configs (one per named block)
|
|
305
|
+
const deploys = {};
|
|
306
|
+
if (deployBlocks.length > 0) {
|
|
307
|
+
const Deploy = getDeployCodegen();
|
|
308
|
+
const deployGroups = this._groupByName(deployBlocks);
|
|
309
|
+
for (const [name, blocks] of deployGroups) {
|
|
310
|
+
const key = name || 'default';
|
|
311
|
+
deploys[key] = Deploy.mergeDeployBlocks(blocks);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
235
315
|
// Generate tests if test blocks exist
|
|
236
316
|
let testCode = '';
|
|
237
317
|
if (testBlocks.length > 0) {
|
|
@@ -267,6 +347,7 @@ export class CodeGenerator {
|
|
|
267
347
|
browser: browserCode,
|
|
268
348
|
client: browserCode, // deprecated alias for backward compat
|
|
269
349
|
edge: edges['default'] || '',
|
|
350
|
+
deploy: Object.keys(deploys).length > 0 ? deploys : undefined,
|
|
270
351
|
sourceMappings,
|
|
271
352
|
_sourceFile: this.filename,
|
|
272
353
|
};
|
|
@@ -287,6 +368,7 @@ export class CodeGenerator {
|
|
|
287
368
|
browsers, // { "admin": code, "dashboard": code, ... }
|
|
288
369
|
clients: browsers, // deprecated alias for backward compat
|
|
289
370
|
edges, // { "api": code, "assets": code, ... }
|
|
371
|
+
deploy: Object.keys(deploys).length > 0 ? deploys : undefined,
|
|
290
372
|
multiBlock: true,
|
|
291
373
|
sourceMappings,
|
|
292
374
|
_sourceFile: this.filename,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Deploy-specific codegen for the Tova language
|
|
2
|
+
// Produces a configuration manifest (plain JS object) from deploy block AST nodes.
|
|
3
|
+
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
instances: 1,
|
|
6
|
+
memory: '512mb',
|
|
7
|
+
branch: 'main',
|
|
8
|
+
health: '/healthz',
|
|
9
|
+
health_interval: 30,
|
|
10
|
+
health_timeout: 5,
|
|
11
|
+
restart_on_failure: true,
|
|
12
|
+
keep_releases: 5,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class DeployCodegen {
|
|
16
|
+
static mergeDeployBlocks(blocks) {
|
|
17
|
+
const config = { ...DEFAULTS, env: {}, databases: [] };
|
|
18
|
+
for (const block of blocks) {
|
|
19
|
+
config.name = block.name;
|
|
20
|
+
for (const stmt of block.body) {
|
|
21
|
+
switch (stmt.type) {
|
|
22
|
+
case 'DeployConfigField': {
|
|
23
|
+
// Extract literal value from AST expression
|
|
24
|
+
const val = stmt.value;
|
|
25
|
+
config[stmt.key] = val.value !== undefined ? val.value : val;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
case 'DeployEnvBlock': {
|
|
29
|
+
for (const entry of stmt.entries) {
|
|
30
|
+
config.env[entry.key] = entry.value.value !== undefined ? entry.value.value : entry.value;
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case 'DeployDbBlock': {
|
|
35
|
+
const dbConfig = {};
|
|
36
|
+
if (stmt.config && typeof stmt.config === 'object') {
|
|
37
|
+
for (const [k, v] of Object.entries(stmt.config)) {
|
|
38
|
+
dbConfig[k] = v.value !== undefined ? v.value : v;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
config.databases.push({ engine: stmt.engine, config: dbConfig });
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return config;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1052,6 +1052,22 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
1052
1052
|
// ════════════════════════════════════════════════════════════
|
|
1053
1053
|
// 8b. Security Headers — OWASP recommended
|
|
1054
1054
|
// ════════════════════════════════════════════════════════════
|
|
1055
|
+
// Always emit base security headers (even in fast mode)
|
|
1056
|
+
lines.push('// ── Base Security Headers (always) ──');
|
|
1057
|
+
lines.push('const __baseSecurityHeaders = Object.freeze({');
|
|
1058
|
+
lines.push(' "X-Content-Type-Options": "nosniff",');
|
|
1059
|
+
lines.push(' "X-Frame-Options": "DENY",');
|
|
1060
|
+
lines.push(' "X-XSS-Protection": "0",');
|
|
1061
|
+
lines.push(' "Referrer-Policy": "strict-origin-when-cross-origin",');
|
|
1062
|
+
lines.push('});');
|
|
1063
|
+
lines.push('function __applyBaseHeaders(res) {');
|
|
1064
|
+
lines.push(' if (!res) return res;');
|
|
1065
|
+
lines.push(' const h = new Headers(res.headers);');
|
|
1066
|
+
lines.push(' for (const [k, v] of Object.entries(__baseSecurityHeaders)) h.set(k, v);');
|
|
1067
|
+
lines.push(' return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h });');
|
|
1068
|
+
lines.push('}');
|
|
1069
|
+
lines.push('');
|
|
1070
|
+
|
|
1055
1071
|
if (!isFastMode) {
|
|
1056
1072
|
lines.push('// ── Security Headers ──');
|
|
1057
1073
|
lines.push('const __securityHeaders = Object.freeze({');
|
|
@@ -2365,7 +2381,12 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2365
2381
|
const rlWindow = rateLimitDec.args[1] ? this.genExpression(rateLimitDec.args[1]) : '60';
|
|
2366
2382
|
lines.push(` const __rlIp = __getClientIp(req);`);
|
|
2367
2383
|
lines.push(` const __rlRoute = __checkRateLimit(\`route:${path}:\${__rlIp}\`, ${rlMax}, ${rlWindow});`);
|
|
2368
|
-
lines.push(` if (__rlRoute.limited)
|
|
2384
|
+
lines.push(` if (__rlRoute.limited) {`);
|
|
2385
|
+
if (securityFragments && securityFragments.auditCode) {
|
|
2386
|
+
lines.push(` __auditLog("rate_limit:exceeded", { method: req.method, path: __pathname }, { id: null });`);
|
|
2387
|
+
}
|
|
2388
|
+
lines.push(` return __errorResponse(429, "RATE_LIMITED", "Too Many Requests", null, { "Retry-After": String(__rlRoute.retryAfter) });`);
|
|
2389
|
+
lines.push(` }`);
|
|
2369
2390
|
}
|
|
2370
2391
|
|
|
2371
2392
|
// Upload decorator — parse multipart body, validate file field
|
|
@@ -3133,8 +3154,8 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
3133
3154
|
}
|
|
3134
3155
|
}
|
|
3135
3156
|
|
|
3136
|
-
// Client HTML fallback
|
|
3137
|
-
lines.push(' if (
|
|
3157
|
+
// Client HTML fallback — serve SPA shell for any non-API route so client router handles 404s
|
|
3158
|
+
lines.push(' if (typeof __clientHTML !== "undefined") {');
|
|
3138
3159
|
lines.push(' return new Response(__clientHTML, { status: 200, headers: { "Content-Type": "text/html" } });');
|
|
3139
3160
|
lines.push(' }');
|
|
3140
3161
|
lines.push(' return new Response("Not Found", { status: 404 });');
|
|
@@ -3217,6 +3238,9 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
3217
3238
|
lines.push(' const __clientIp = __getClientIp(req);');
|
|
3218
3239
|
lines.push(' const __rl = __checkRateLimit(__clientIp, __rateLimitMax, __rateLimitWindow);');
|
|
3219
3240
|
lines.push(' if (__rl.limited) {');
|
|
3241
|
+
if (securityFragments && securityFragments.auditCode) {
|
|
3242
|
+
lines.push(' __auditLog("rate_limit:exceeded", { method: req.method, path: __pathname, ip: __clientIp }, { id: null });');
|
|
3243
|
+
}
|
|
3220
3244
|
lines.push(' return __errorResponse(429, "RATE_LIMITED", "Too Many Requests", null, { ...__cors, "Retry-After": String(__rl.retryAfter) });');
|
|
3221
3245
|
lines.push(' }');
|
|
3222
3246
|
}
|
|
@@ -3266,6 +3290,10 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
3266
3290
|
lines.push(' // Security block: route protection');
|
|
3267
3291
|
if (authConfig) {
|
|
3268
3292
|
lines.push(' const __secUser = await __authenticate(req);');
|
|
3293
|
+
if (securityFragments && securityFragments.auditCode) {
|
|
3294
|
+
lines.push(' if (__secUser) __auditLog("auth:success", { method: req.method, path: __pathname }, __secUser);');
|
|
3295
|
+
lines.push(' else if (req.headers.get("Authorization")) __auditLog("auth:failure", { method: req.method, path: __pathname, reason: "invalid_token" }, { id: null });');
|
|
3296
|
+
}
|
|
3269
3297
|
} else {
|
|
3270
3298
|
lines.push(' const __secUser = null;');
|
|
3271
3299
|
}
|
|
@@ -3273,17 +3301,31 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
3273
3301
|
lines.push(' if (!__protection.allowed) {');
|
|
3274
3302
|
lines.push(' const __statusCode = __secUser ? 403 : 401;');
|
|
3275
3303
|
lines.push(' const __errorCode = __secUser ? "FORBIDDEN" : "AUTH_REQUIRED";');
|
|
3304
|
+
if (securityFragments && securityFragments.auditCode) {
|
|
3305
|
+
lines.push(' __auditLog("auth:denied", { method: req.method, path: __pathname }, __secUser || { id: null });');
|
|
3306
|
+
}
|
|
3276
3307
|
lines.push(' return __errorResponse(__statusCode, __errorCode, __protection.reason, null, __cors);');
|
|
3277
3308
|
lines.push(' }');
|
|
3278
3309
|
lines.push(' if (__protection.rateLimit && __protection.rateLimit.max) {');
|
|
3279
3310
|
lines.push(' const __protectIp = __getClientIp(req);');
|
|
3280
3311
|
lines.push(' const __protectRl = __checkRateLimit("protect:" + __pathname + ":" + __protectIp, __protection.rateLimit.max, __protection.rateLimit.window || 60);');
|
|
3281
3312
|
lines.push(' if (__protectRl.limited) {');
|
|
3313
|
+
if (securityFragments && securityFragments.auditCode) {
|
|
3314
|
+
lines.push(' __auditLog("rate_limit:exceeded", { method: req.method, path: __pathname, ip: __protectIp }, { id: null });');
|
|
3315
|
+
}
|
|
3282
3316
|
lines.push(' return __errorResponse(429, "RATE_LIMITED", "Too Many Requests", null, { ...__cors, "Retry-After": String(__protectRl.retryAfter) });');
|
|
3283
3317
|
lines.push(' }');
|
|
3284
3318
|
lines.push(' }');
|
|
3285
3319
|
}
|
|
3286
3320
|
|
|
3321
|
+
// Audit logging for auth (when no protection block but auth+audit configured)
|
|
3322
|
+
if (!(securityFragments && securityFragments.protectCode) && authConfig && securityFragments && securityFragments.auditCode) {
|
|
3323
|
+
lines.push(' // Audit: auth logging');
|
|
3324
|
+
lines.push(' const __secUser = await __authenticate(req);');
|
|
3325
|
+
lines.push(' if (__secUser) __auditLog("auth:success", { method: req.method, path: __pathname }, __secUser);');
|
|
3326
|
+
lines.push(' else if (req.headers.get("Authorization")) __auditLog("auth:failure", { method: req.method, path: __pathname, reason: "invalid_token" }, { id: null });');
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3287
3329
|
// Idempotency key check
|
|
3288
3330
|
lines.push(' const __idempotencyKey = req.headers.get("Idempotency-Key");');
|
|
3289
3331
|
lines.push(' if (__idempotencyKey && req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS") {');
|
|
@@ -3422,8 +3464,8 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
3422
3464
|
lines.push(' }');
|
|
3423
3465
|
lines.push(' }');
|
|
3424
3466
|
|
|
3425
|
-
// Serve client HTML
|
|
3426
|
-
lines.push(' if (
|
|
3467
|
+
// Serve client HTML — SPA fallback for any non-API route so client router handles 404s
|
|
3468
|
+
lines.push(' if (typeof __clientHTML !== "undefined") {');
|
|
3427
3469
|
lines.push(' return new Response(__clientHTML, { status: 200, headers: { "Content-Type": "text/html", ...(__cors) } });');
|
|
3428
3470
|
lines.push(' }');
|
|
3429
3471
|
lines.push(' const __notFound = __errorResponse(404, "NOT_FOUND", "Not Found", null, __cors);');
|
|
@@ -3490,8 +3532,14 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
3490
3532
|
lines.push(' if (!res) return res;');
|
|
3491
3533
|
lines.push(' return __compressResponse(req, res);');
|
|
3492
3534
|
lines.push('};');
|
|
3535
|
+
} else if (isFastMode) {
|
|
3536
|
+
// Fast mode: wrap with base security headers
|
|
3537
|
+
lines.push('const __secureFetch = async (req) => {');
|
|
3538
|
+
lines.push(' const res = await __handleRequest(req);');
|
|
3539
|
+
lines.push(' return __applyBaseHeaders(res);');
|
|
3540
|
+
lines.push('};');
|
|
3493
3541
|
}
|
|
3494
|
-
const fetchHandler =
|
|
3542
|
+
const fetchHandler = !isFastMode ? '__idempotentFetch' : (compressionConfig ? '__idempotentFetch' : '__secureFetch');
|
|
3495
3543
|
lines.push(`const __server = Bun.serve({`);
|
|
3496
3544
|
lines.push(` port: __port,`);
|
|
3497
3545
|
lines.push(` maxRequestBodySize: __maxBodySize,`);
|
|
@@ -10,6 +10,11 @@ export class SharedCodegen extends BaseCodegen {
|
|
|
10
10
|
// Generate any needed helpers (called after all code is generated)
|
|
11
11
|
generateHelpers() {
|
|
12
12
|
const helpers = [];
|
|
13
|
+
// Runtime bridge for WASM-Tokio concurrent execution
|
|
14
|
+
if (this._needsRuntimeBridge) {
|
|
15
|
+
// Try multiple paths: relative to script, package require, absolute from process.cwd()
|
|
16
|
+
helpers.push(`let __tova_rt = null; try { const __p = require('path'); const __d = __p.dirname(typeof __filename !== 'undefined' ? __filename : process.argv[1] || ''); const __candidates = [__p.join(__d, '..', 'src', 'stdlib', 'runtime-bridge.js'), __p.join(process.cwd(), 'src', 'stdlib', 'runtime-bridge.js')]; for (const __c of __candidates) { try { __tova_rt = require(__c); break; } catch(_) {} } } catch(_) {}`);
|
|
17
|
+
}
|
|
13
18
|
helpers.push(this.getStringProtoHelper());
|
|
14
19
|
// Only include Result/Option if Ok/Err/Some/None are used
|
|
15
20
|
if (this._needsResultOption) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Theme codegen: converts ThemeBlock AST to CSS custom properties
|
|
2
|
+
|
|
3
|
+
const PX_SECTIONS = new Set(['spacing', 'radius']);
|
|
4
|
+
const PX_FONT_PREFIXES = ['size.'];
|
|
5
|
+
|
|
6
|
+
const CATEGORY_MAP = {
|
|
7
|
+
colors: 'color',
|
|
8
|
+
spacing: 'spacing',
|
|
9
|
+
radius: 'radius',
|
|
10
|
+
shadow: 'shadow',
|
|
11
|
+
font: 'font',
|
|
12
|
+
breakpoints: 'breakpoint',
|
|
13
|
+
transition: 'transition',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class ThemeCodegen {
|
|
17
|
+
static mergeThemeBlocks(themeBlocks) {
|
|
18
|
+
const sections = new Map();
|
|
19
|
+
const darkOverrides = [];
|
|
20
|
+
for (const block of themeBlocks) {
|
|
21
|
+
for (const section of block.sections) {
|
|
22
|
+
if (!sections.has(section.name)) sections.set(section.name, []);
|
|
23
|
+
sections.get(section.name).push(...section.tokens);
|
|
24
|
+
}
|
|
25
|
+
darkOverrides.push(...block.darkOverrides);
|
|
26
|
+
}
|
|
27
|
+
return { sections, darkOverrides };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static generateCSS(themeConfig) {
|
|
31
|
+
const { sections, darkOverrides } = themeConfig;
|
|
32
|
+
const rootProps = [];
|
|
33
|
+
const darkProps = [];
|
|
34
|
+
|
|
35
|
+
for (const [sectionName, tokens] of sections) {
|
|
36
|
+
const prefix = CATEGORY_MAP[sectionName] || sectionName;
|
|
37
|
+
for (const token of tokens) {
|
|
38
|
+
const cssName = `--tova-${prefix}-${token.name.replace(/\./g, '-')}`;
|
|
39
|
+
const cssValue = ThemeCodegen._formatValue(sectionName, token.name, token.value);
|
|
40
|
+
rootProps.push(` ${cssName}: ${cssValue};`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const override of darkOverrides) {
|
|
45
|
+
const dotIdx = override.name.indexOf('.');
|
|
46
|
+
const sectionName = override.name.slice(0, dotIdx);
|
|
47
|
+
const tokenName = override.name.slice(dotIdx + 1);
|
|
48
|
+
const prefix = CATEGORY_MAP[sectionName] || sectionName;
|
|
49
|
+
const cssName = `--tova-${prefix}-${tokenName.replace(/\./g, '-')}`;
|
|
50
|
+
const cssValue = ThemeCodegen._formatValue(sectionName, tokenName, override.value);
|
|
51
|
+
darkProps.push(` ${cssName}: ${cssValue};`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let css = `:root {\n${rootProps.join('\n')}\n}`;
|
|
55
|
+
if (darkProps.length > 0) {
|
|
56
|
+
css += `\n@media (prefers-color-scheme: dark) {\n :root {\n${darkProps.join('\n')}\n }\n}`;
|
|
57
|
+
}
|
|
58
|
+
return css;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static _formatValue(sectionName, tokenName, value) {
|
|
62
|
+
if (typeof value === 'number') {
|
|
63
|
+
if (PX_SECTIONS.has(sectionName)) return value + 'px';
|
|
64
|
+
if (sectionName === 'font' && PX_FONT_PREFIXES.some(p => tokenName.startsWith(p))) return value + 'px';
|
|
65
|
+
return String(value);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -602,6 +602,12 @@ export function generateWasmGlue(funcNode, wasmBytes) {
|
|
|
602
602
|
return `const ${name} = new WebAssembly.Instance(new WebAssembly.Module(new Uint8Array([${bytesStr}]))).exports.${name};`;
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
+
// Generate a module-level constant holding the raw WASM bytes for runtime use
|
|
606
|
+
export function generateWasmBytesExport(funcName, wasmBytes) {
|
|
607
|
+
const bytesStr = Array.from(wasmBytes).join(',');
|
|
608
|
+
return `const __wasm_bytes_${funcName} = new Uint8Array([${bytesStr}]);`;
|
|
609
|
+
}
|
|
610
|
+
|
|
605
611
|
// Generate JS glue code for a multi-function WASM module
|
|
606
612
|
export function generateMultiWasmGlue(funcNodes, wasmBytes) {
|
|
607
613
|
const bytesStr = Array.from(wasmBytes).join(',');
|
package/src/config/edit-toml.js
CHANGED
|
@@ -28,13 +28,15 @@ export function addToSection(filePath, section, key, value) {
|
|
|
28
28
|
const endIdx = findSectionEnd(lines, sectionIdx);
|
|
29
29
|
|
|
30
30
|
// Check if key already exists in this section — update it
|
|
31
|
+
const bareKey = key.replace(/^"|"$/g, '');
|
|
31
32
|
for (let i = sectionIdx + 1; i < endIdx; i++) {
|
|
32
33
|
const line = lines[i].trim();
|
|
33
34
|
if (line === '' || line.startsWith('#')) continue;
|
|
34
35
|
const eqIdx = line.indexOf('=');
|
|
35
36
|
if (eqIdx !== -1) {
|
|
36
37
|
const existingKey = line.slice(0, eqIdx).trim();
|
|
37
|
-
|
|
38
|
+
const existingBare = existingKey.replace(/^"|"$/g, '');
|
|
39
|
+
if (existingKey === key || existingBare === bareKey) {
|
|
38
40
|
lines[i] = entry;
|
|
39
41
|
writeFileSync(filePath, lines.join('\n'));
|
|
40
42
|
return;
|
|
@@ -57,6 +59,7 @@ export function addToSection(filePath, section, key, value) {
|
|
|
57
59
|
export function removeFromSection(filePath, section, key) {
|
|
58
60
|
const content = readFileSync(filePath, 'utf-8');
|
|
59
61
|
const lines = content.split('\n');
|
|
62
|
+
const bareKey = key.replace(/^"|"$/g, '');
|
|
60
63
|
|
|
61
64
|
const sectionIdx = findSectionIndex(lines, section);
|
|
62
65
|
if (sectionIdx === -1) return false;
|
|
@@ -69,7 +72,8 @@ export function removeFromSection(filePath, section, key) {
|
|
|
69
72
|
const eqIdx = line.indexOf('=');
|
|
70
73
|
if (eqIdx !== -1) {
|
|
71
74
|
const existingKey = line.slice(0, eqIdx).trim();
|
|
72
|
-
|
|
75
|
+
const existingBare = existingKey.replace(/^"|"$/g, '');
|
|
76
|
+
if (existingKey === key || existingBare === bareKey) {
|
|
73
77
|
lines.splice(i, 1);
|
|
74
78
|
writeFileSync(filePath, lines.join('\n'));
|
|
75
79
|
return true;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Git resolver for the Tova package manager.
|
|
2
|
+
// Handles git operations: parsing tag lists, sorting by semver,
|
|
3
|
+
// picking the latest tag, and fetching modules from remote repositories.
|
|
4
|
+
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { mkdirSync, renameSync, rmSync, existsSync } from 'fs';
|
|
8
|
+
import { parseSemver, compareSemver } from './semver.js';
|
|
9
|
+
import { moduleToGitUrl, parseModulePath } from './module-path.js';
|
|
10
|
+
import { getModuleCachePath, getCacheDir } from './module-cache.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse `git ls-remote --tags` output into an array of { version, sha } objects.
|
|
14
|
+
* Filters out non-semver tags and prefers dereferenced (^{}) SHAs for annotated tags.
|
|
15
|
+
*/
|
|
16
|
+
export function parseTagList(output) {
|
|
17
|
+
if (!output.trim()) return [];
|
|
18
|
+
const lines = output.trim().split('\n');
|
|
19
|
+
const tagMap = new Map();
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const [sha, ref] = line.split('\t');
|
|
22
|
+
if (!ref || !ref.startsWith('refs/tags/')) continue;
|
|
23
|
+
const tagName = ref.replace('refs/tags/', '');
|
|
24
|
+
const isDeref = tagName.endsWith('^{}');
|
|
25
|
+
const cleanName = isDeref ? tagName.slice(0, -3) : tagName;
|
|
26
|
+
const versionStr = cleanName.startsWith('v') ? cleanName.slice(1) : cleanName;
|
|
27
|
+
try { parseSemver(versionStr); } catch { continue; }
|
|
28
|
+
if (isDeref || !tagMap.has(versionStr)) {
|
|
29
|
+
tagMap.set(versionStr, sha);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return Array.from(tagMap.entries()).map(([version, sha]) => ({ version, sha }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sort an array of { version, sha } tags by semver in ascending order.
|
|
37
|
+
* Returns a new array (does not mutate the input).
|
|
38
|
+
*/
|
|
39
|
+
export function sortTags(tags) {
|
|
40
|
+
return [...tags].sort((a, b) => compareSemver(a.version, b.version));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pick the tag with the highest semver version.
|
|
45
|
+
* Returns null if the tags array is empty.
|
|
46
|
+
*/
|
|
47
|
+
export function pickLatestTag(tags) {
|
|
48
|
+
if (tags.length === 0) return null;
|
|
49
|
+
const sorted = sortTags(tags);
|
|
50
|
+
return sorted[sorted.length - 1];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* List all semver tags from a remote git repository.
|
|
55
|
+
* Returns a promise resolving to an array of { version, sha } objects.
|
|
56
|
+
*/
|
|
57
|
+
export function listRemoteTags(modulePath) {
|
|
58
|
+
const gitUrl = moduleToGitUrl(modulePath);
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const proc = spawn('git', ['ls-remote', '--tags', gitUrl], {
|
|
61
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
62
|
+
});
|
|
63
|
+
let stdout = '';
|
|
64
|
+
let stderr = '';
|
|
65
|
+
proc.stdout.on('data', d => stdout += d);
|
|
66
|
+
proc.stderr.on('data', d => stderr += d);
|
|
67
|
+
proc.on('close', code => {
|
|
68
|
+
if (code !== 0) {
|
|
69
|
+
reject(new Error(`Failed to list tags for ${modulePath}: ${stderr.trim()}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
resolve(parseTagList(stdout));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Clone a specific version of a module into the local cache.
|
|
79
|
+
* Performs a shallow clone, removes .git directory, and moves to the cache path.
|
|
80
|
+
* Returns the destination path on success.
|
|
81
|
+
*/
|
|
82
|
+
export function fetchModule(modulePath, version, cacheDir) {
|
|
83
|
+
const gitUrl = moduleToGitUrl(modulePath);
|
|
84
|
+
const destPath = getModuleCachePath(modulePath, version, cacheDir);
|
|
85
|
+
const dir = getCacheDir(cacheDir);
|
|
86
|
+
const tmpPath = join(dir, '.tmp-' + Date.now());
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const proc = spawn('git', [
|
|
89
|
+
'clone', '--depth', '1', '--branch', `v${version}`, gitUrl, tmpPath,
|
|
90
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
91
|
+
let stderr = '';
|
|
92
|
+
proc.stderr.on('data', d => stderr += d);
|
|
93
|
+
proc.on('close', code => {
|
|
94
|
+
if (code !== 0) {
|
|
95
|
+
if (existsSync(tmpPath)) rmSync(tmpPath, { recursive: true, force: true });
|
|
96
|
+
reject(new Error(`Failed to fetch ${modulePath}@v${version}: ${stderr.trim()}`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const dotGit = join(tmpPath, '.git');
|
|
100
|
+
if (existsSync(dotGit)) rmSync(dotGit, { recursive: true, force: true });
|
|
101
|
+
mkdirSync(join(destPath, '..'), { recursive: true });
|
|
102
|
+
renameSync(tmpPath, destPath);
|
|
103
|
+
resolve(destPath);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the commit SHA for a specific version tag from a remote repository.
|
|
110
|
+
* Prefers the dereferenced SHA for annotated tags.
|
|
111
|
+
* Returns null if the version tag is not found.
|
|
112
|
+
*/
|
|
113
|
+
export function getCommitSha(modulePath, version, cacheDir) {
|
|
114
|
+
const gitUrl = moduleToGitUrl(modulePath);
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const proc = spawn('git', ['ls-remote', gitUrl, `refs/tags/v${version}^{}`, `refs/tags/v${version}`], {
|
|
117
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
|
+
});
|
|
119
|
+
let stdout = '';
|
|
120
|
+
proc.stdout.on('data', d => stdout += d);
|
|
121
|
+
proc.on('close', code => {
|
|
122
|
+
if (code !== 0) { reject(new Error('Failed to get SHA')); return; }
|
|
123
|
+
const tags = parseTagList(stdout);
|
|
124
|
+
const tag = tags.find(t => t.version === version);
|
|
125
|
+
resolve(tag ? tag.sha : null);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/config/lock-file.js
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { parseTOML } from './toml.js';
|
|
5
|
+
|
|
6
|
+
export function writeLockFile(cwd, resolvedModules, npmDeps) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
lines.push('[lock]');
|
|
9
|
+
lines.push(`generated = "${new Date().toISOString()}"`);
|
|
10
|
+
lines.push('');
|
|
11
|
+
|
|
12
|
+
for (const [mod, info] of Object.entries(resolvedModules)) {
|
|
13
|
+
lines.push(`["${mod}"]`);
|
|
14
|
+
lines.push(`version = "${info.version}"`);
|
|
15
|
+
lines.push(`sha = "${info.sha}"`);
|
|
16
|
+
lines.push(`source = "${info.source}"`);
|
|
17
|
+
lines.push('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (npmDeps && Object.keys(npmDeps).length > 0) {
|
|
21
|
+
lines.push('[npm]');
|
|
22
|
+
for (const [name, version] of Object.entries(npmDeps)) {
|
|
23
|
+
lines.push(`${name} = "${version}"`);
|
|
24
|
+
}
|
|
25
|
+
lines.push('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
writeFileSync(join(cwd, 'tova.lock'), lines.join('\n'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readLockFile(cwd) {
|
|
32
|
+
const lockPath = join(cwd, 'tova.lock');
|
|
33
|
+
if (!existsSync(lockPath)) return null;
|
|
34
|
+
const content = readFileSync(lockPath, 'utf-8');
|
|
35
|
+
const parsed = parseTOML(content);
|
|
36
|
+
|
|
37
|
+
const modules = {};
|
|
38
|
+
const npm = {};
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
41
|
+
if (key === 'lock') continue;
|
|
42
|
+
if (key === 'npm') {
|
|
43
|
+
Object.assign(npm, value);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Module entries are quoted keys like "github.com/alice/http"
|
|
47
|
+
if (typeof value === 'object' && value.version) {
|
|
48
|
+
modules[key] = {
|
|
49
|
+
version: value.version,
|
|
50
|
+
sha: value.sha,
|
|
51
|
+
source: value.source,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { modules, npm, generated: parsed.lock?.generated };
|
|
57
|
+
}
|