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.
Files changed (59) hide show
  1. package/bin/tova.js +1312 -139
  2. package/package.json +8 -1
  3. package/src/analyzer/analyzer.js +539 -11
  4. package/src/analyzer/browser-analyzer.js +56 -8
  5. package/src/analyzer/deploy-analyzer.js +44 -0
  6. package/src/analyzer/scope.js +7 -0
  7. package/src/analyzer/server-analyzer.js +33 -1
  8. package/src/codegen/base-codegen.js +1296 -23
  9. package/src/codegen/browser-codegen.js +725 -20
  10. package/src/codegen/codegen.js +87 -5
  11. package/src/codegen/deploy-codegen.js +49 -0
  12. package/src/codegen/server-codegen.js +54 -6
  13. package/src/codegen/shared-codegen.js +5 -0
  14. package/src/codegen/theme-codegen.js +69 -0
  15. package/src/codegen/wasm-codegen.js +6 -0
  16. package/src/config/edit-toml.js +6 -2
  17. package/src/config/git-resolver.js +128 -0
  18. package/src/config/lock-file.js +57 -0
  19. package/src/config/module-cache.js +58 -0
  20. package/src/config/module-entry.js +37 -0
  21. package/src/config/module-path.js +63 -0
  22. package/src/config/pkg-errors.js +62 -0
  23. package/src/config/resolve.js +26 -0
  24. package/src/config/resolver.js +139 -0
  25. package/src/config/search.js +28 -0
  26. package/src/config/semver.js +72 -0
  27. package/src/config/toml.js +61 -6
  28. package/src/deploy/deploy.js +217 -0
  29. package/src/deploy/infer.js +218 -0
  30. package/src/deploy/provision.js +315 -0
  31. package/src/diagnostics/security-scorecard.js +111 -0
  32. package/src/lexer/lexer.js +18 -3
  33. package/src/lsp/server.js +482 -0
  34. package/src/parser/animate-ast.js +45 -0
  35. package/src/parser/ast.js +39 -0
  36. package/src/parser/browser-ast.js +19 -1
  37. package/src/parser/browser-parser.js +221 -4
  38. package/src/parser/concurrency-ast.js +15 -0
  39. package/src/parser/concurrency-parser.js +236 -0
  40. package/src/parser/deploy-ast.js +37 -0
  41. package/src/parser/deploy-parser.js +132 -0
  42. package/src/parser/parser.js +42 -5
  43. package/src/parser/select-ast.js +39 -0
  44. package/src/parser/theme-ast.js +29 -0
  45. package/src/parser/theme-parser.js +70 -0
  46. package/src/registry/plugins/concurrency-plugin.js +32 -0
  47. package/src/registry/plugins/deploy-plugin.js +33 -0
  48. package/src/registry/plugins/theme-plugin.js +20 -0
  49. package/src/registry/register-all.js +6 -0
  50. package/src/runtime/charts.js +547 -0
  51. package/src/runtime/embedded.js +6 -2
  52. package/src/runtime/reactivity.js +60 -0
  53. package/src/runtime/router.js +703 -295
  54. package/src/runtime/table.js +606 -33
  55. package/src/stdlib/inline.js +365 -10
  56. package/src/stdlib/runtime-bridge.js +152 -0
  57. package/src/stdlib/string.js +84 -2
  58. package/src/stdlib/validation.js +1 -1
  59. package/src/version.js +1 -1
@@ -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
- // Use genBlockStatements for pattern optimization (array fill detection, etc.)
100
- const fakeBlock = { type: 'BlockStatement', body: topLevel };
101
- const moduleCode = moduleGen.genBlockStatements(fakeBlock);
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 combined = [helpers, moduleCode].filter(s => s.trim()).join('\n').trim();
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) return __errorResponse(429, "RATE_LIMITED", "Too Many Requests", null, { "Retry-After": String(__rlRoute.retryAfter) });`);
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 (__pathname === "/" && typeof __clientHTML !== "undefined") {');
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 at root
3426
- lines.push(' if (__pathname === "/" && typeof __clientHTML !== "undefined") {');
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 = (!isFastMode || compressionConfig) ? '__idempotentFetch' : '__handleRequest';
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(',');
@@ -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
- if (existingKey === key) {
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
- if (existingKey === key) {
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
+ }