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.
Files changed (60) hide show
  1. package/bin/tova.js +261 -60
  2. package/package.json +1 -1
  3. package/src/analyzer/analyzer.js +351 -11
  4. package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
  5. package/src/analyzer/deploy-analyzer.js +44 -0
  6. package/src/analyzer/form-analyzer.js +113 -0
  7. package/src/analyzer/scope.js +2 -2
  8. package/src/codegen/base-codegen.js +1160 -10
  9. package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
  10. package/src/codegen/codegen.js +119 -28
  11. package/src/codegen/deploy-codegen.js +49 -0
  12. package/src/codegen/edge-codegen.js +1351 -0
  13. package/src/codegen/form-codegen.js +553 -0
  14. package/src/codegen/security-codegen.js +5 -5
  15. package/src/codegen/server-codegen.js +88 -7
  16. package/src/codegen/shared-codegen.js +5 -0
  17. package/src/codegen/wasm-codegen.js +6 -0
  18. package/src/config/edit-toml.js +6 -2
  19. package/src/config/git-resolver.js +128 -0
  20. package/src/config/lock-file.js +57 -0
  21. package/src/config/module-cache.js +58 -0
  22. package/src/config/module-entry.js +37 -0
  23. package/src/config/module-path.js +31 -0
  24. package/src/config/pkg-errors.js +62 -0
  25. package/src/config/resolve.js +17 -0
  26. package/src/config/resolver.js +139 -0
  27. package/src/config/search.js +28 -0
  28. package/src/config/semver.js +72 -0
  29. package/src/config/toml.js +48 -5
  30. package/src/deploy/deploy.js +217 -0
  31. package/src/deploy/infer.js +218 -0
  32. package/src/deploy/provision.js +311 -0
  33. package/src/diagnostics/error-codes.js +1 -1
  34. package/src/docs/generator.js +1 -1
  35. package/src/formatter/formatter.js +4 -4
  36. package/src/lexer/tokens.js +12 -2
  37. package/src/lsp/server.js +483 -1
  38. package/src/parser/ast.js +60 -5
  39. package/src/parser/{client-ast.js → browser-ast.js} +3 -3
  40. package/src/parser/{client-parser.js → browser-parser.js} +42 -15
  41. package/src/parser/concurrency-ast.js +15 -0
  42. package/src/parser/concurrency-parser.js +236 -0
  43. package/src/parser/deploy-ast.js +37 -0
  44. package/src/parser/deploy-parser.js +132 -0
  45. package/src/parser/edge-ast.js +83 -0
  46. package/src/parser/edge-parser.js +262 -0
  47. package/src/parser/form-ast.js +80 -0
  48. package/src/parser/form-parser.js +206 -0
  49. package/src/parser/parser.js +82 -14
  50. package/src/parser/select-ast.js +39 -0
  51. package/src/registry/plugins/browser-plugin.js +30 -0
  52. package/src/registry/plugins/concurrency-plugin.js +32 -0
  53. package/src/registry/plugins/deploy-plugin.js +33 -0
  54. package/src/registry/plugins/edge-plugin.js +32 -0
  55. package/src/registry/register-all.js +8 -2
  56. package/src/runtime/ssr.js +2 -2
  57. package/src/stdlib/inline.js +38 -6
  58. package/src/stdlib/runtime-bridge.js +152 -0
  59. package/src/version.js +1 -1
  60. package/src/registry/plugins/client-plugin.js +0 -30
package/bin/tova.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { resolve, basename, dirname, join, relative } from 'path';
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync, rmSync, chmodSync, renameSync, watch as fsWatch } from 'fs';
5
5
  import { spawn } from 'child_process';
6
- // Bun.hash used instead of crypto.createHash for faster hashing
6
+ import { createHash as _cryptoHash } from 'crypto';
7
7
  import { Lexer } from '../src/lexer/lexer.js';
8
8
  import { Parser } from '../src/parser/parser.js';
9
9
  import { Analyzer } from '../src/analyzer/analyzer.js';
@@ -70,6 +70,8 @@ Commands:
70
70
  info Show Tova version, Bun version, project config, and installed dependencies
71
71
  doctor Check your development environment
72
72
  completions <sh> Generate shell completions (bash, zsh, fish)
73
+ deploy <env> Deploy to a server (--plan, --rollback, --logs, --status)
74
+ env <env> <cmd> Manage secrets (list, set KEY=value)
73
75
  explain <code> Show detailed explanation for an error/warning code (e.g., tova explain E202)
74
76
 
75
77
  Options:
@@ -143,6 +145,74 @@ async function main() {
143
145
  case 'remove':
144
146
  await removeDep(args[1]);
145
147
  break;
148
+ case 'update': {
149
+ const updatePkg = args[1] || null;
150
+ const updateConfig = resolveConfig(process.cwd());
151
+ const updateDeps = updatePkg
152
+ ? { [updatePkg]: updateConfig.dependencies?.[updatePkg] || '*' }
153
+ : updateConfig.dependencies || {};
154
+
155
+ if (Object.keys(updateDeps).length === 0) {
156
+ console.log(' No Tova dependencies to update.');
157
+ break;
158
+ }
159
+
160
+ console.log(' Checking for updates...');
161
+ // Delete lock file to force fresh resolution
162
+ const lockPath = join(process.cwd(), 'tova.lock');
163
+ if (existsSync(lockPath)) {
164
+ rmSync(lockPath);
165
+ }
166
+ await installDeps();
167
+ break;
168
+ }
169
+ case 'cache': {
170
+ const subCmd = args[1] || 'list';
171
+ const { getCacheDir } = await import('../src/config/module-cache.js');
172
+ const cacheDir = getCacheDir();
173
+
174
+ if (subCmd === 'path') {
175
+ console.log(cacheDir);
176
+ } else if (subCmd === 'list') {
177
+ console.log(` Cache: ${cacheDir}\n`);
178
+ if (existsSync(cacheDir)) {
179
+ try {
180
+ const hosts = readdirSync(cacheDir).filter(h => !h.startsWith('.'));
181
+ let found = false;
182
+ for (const host of hosts) {
183
+ const hostPath = join(cacheDir, host);
184
+ if (!statSync(hostPath).isDirectory()) continue;
185
+ const owners = readdirSync(hostPath);
186
+ for (const owner of owners) {
187
+ const ownerPath = join(hostPath, owner);
188
+ if (!statSync(ownerPath).isDirectory()) continue;
189
+ const repos = readdirSync(ownerPath);
190
+ for (const repo of repos) {
191
+ const repoPath = join(ownerPath, repo);
192
+ if (!statSync(repoPath).isDirectory()) continue;
193
+ const versions = readdirSync(repoPath).filter(v => v.startsWith('v'));
194
+ console.log(` ${host}/${owner}/${repo}: ${versions.join(', ') || '(empty)'}`);
195
+ found = true;
196
+ }
197
+ }
198
+ }
199
+ if (!found) console.log(' (empty)');
200
+ } catch { console.log(' (empty)'); }
201
+ } else {
202
+ console.log(' (empty)');
203
+ }
204
+ } else if (subCmd === 'clean') {
205
+ if (existsSync(cacheDir)) {
206
+ rmSync(cacheDir, { recursive: true, force: true });
207
+ }
208
+ console.log(' Cache cleared.');
209
+ } else {
210
+ console.error(` Unknown cache subcommand: ${subCmd}`);
211
+ console.error(' Usage: tova cache [list|path|clean]');
212
+ process.exit(1);
213
+ }
214
+ break;
215
+ }
146
216
  case 'fmt':
147
217
  formatFile(args.slice(1));
148
218
  break;
@@ -173,6 +243,9 @@ async function main() {
173
243
  case 'migrate:status':
174
244
  await migrateStatus(args.slice(1));
175
245
  break;
246
+ case 'deploy':
247
+ await deployCommand(args.slice(1));
248
+ break;
176
249
  case 'explain': {
177
250
  const code = args[1];
178
251
  if (!code) {
@@ -546,6 +619,22 @@ function findTovaFiles(dir) {
546
619
  return files;
547
620
  }
548
621
 
622
+ // ─── Deploy ──────────────────────────────────────────────────
623
+
624
+ async function deployCommand(args) {
625
+ const { parseDeployArgs } = await import('../src/deploy/deploy.js');
626
+ const deployArgs = parseDeployArgs(args);
627
+
628
+ if (!deployArgs.envName && !deployArgs.list) {
629
+ console.error(color.red('Error: deploy requires an environment name (e.g., tova deploy prod)'));
630
+ process.exit(1);
631
+ }
632
+
633
+ // For now, just parse and build — full SSH deployment is wired in integration
634
+ console.log(color.cyan('Deploy feature is being implemented...'));
635
+ console.log('Parsed args:', deployArgs);
636
+ }
637
+
549
638
  // ─── Run ────────────────────────────────────────────────────
550
639
 
551
640
  async function runFile(filePath, options = {}) {
@@ -628,7 +717,7 @@ async function runFile(filePath, options = {}) {
628
717
  }
629
718
  }
630
719
 
631
- let code = stdlib + '\n' + depCode + (output.shared || '') + '\n' + (output.server || output.client || '');
720
+ let code = stdlib + '\n' + depCode + (output.shared || '') + '\n' + (output.server || output.browser || '');
632
721
  // Strip 'export ' keywords — not valid inside AsyncFunction (used in tova build only)
633
722
  code = code.replace(/^export /gm, '');
634
723
  // Strip import lines for local modules (already inlined above)
@@ -833,11 +922,18 @@ async function buildProject(args) {
833
922
  if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', serverPath)}${timing}`);
834
923
  }
835
924
 
836
- // Write default client
837
- if (output.client) {
838
- const clientPath = join(outDir, `${outBaseName}.client.js`);
839
- writeFileSync(clientPath, generateSourceMap(output.client, clientPath));
840
- if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', clientPath)}${timing}`);
925
+ // Write default browser
926
+ if (output.browser) {
927
+ const browserPath = join(outDir, `${outBaseName}.browser.js`);
928
+ writeFileSync(browserPath, generateSourceMap(output.browser, browserPath));
929
+ if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', browserPath)}${timing}`);
930
+ }
931
+
932
+ // Write default edge
933
+ if (output.edge) {
934
+ const edgePath = join(outDir, `${outBaseName}.edge.js`);
935
+ writeFileSync(edgePath, generateSourceMap(output.edge, edgePath));
936
+ if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', edgePath)} [edge]${timing}`);
841
937
  }
842
938
 
843
939
  // Write named server blocks (multi-block)
@@ -850,13 +946,23 @@ async function buildProject(args) {
850
946
  }
851
947
  }
852
948
 
853
- // Write named client blocks (multi-block)
854
- if (output.multiBlock && output.clients) {
855
- for (const [name, code] of Object.entries(output.clients)) {
949
+ // Write named edge blocks (multi-block)
950
+ if (output.multiBlock && output.edges) {
951
+ for (const [name, code] of Object.entries(output.edges)) {
952
+ if (name === 'default') continue;
953
+ const path = join(outDir, `${outBaseName}.edge.${name}.js`);
954
+ writeFileSync(path, code);
955
+ if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', path)} [edge:${name}]${timing}`);
956
+ }
957
+ }
958
+
959
+ // Write named browser blocks (multi-block)
960
+ if (output.multiBlock && output.browsers) {
961
+ for (const [name, code] of Object.entries(output.browsers)) {
856
962
  if (name === 'default') continue;
857
- const path = join(outDir, `${outBaseName}.client.${name}.js`);
963
+ const path = join(outDir, `${outBaseName}.browser.${name}.js`);
858
964
  writeFileSync(path, code);
859
- if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', path)} [client:${name}]${timing}`);
965
+ if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', path)} [browser:${name}]${timing}`);
860
966
  }
861
967
  }
862
968
 
@@ -865,7 +971,7 @@ async function buildProject(args) {
865
971
  const outputPaths = {};
866
972
  if (output.shared && output.shared.trim()) outputPaths.shared = join(outDir, `${outBaseName}.shared.js`);
867
973
  if (output.server) outputPaths.server = join(outDir, `${outBaseName}.server.js`);
868
- if (output.client) outputPaths.client = join(outDir, `${outBaseName}.client.js`);
974
+ if (output.browser) outputPaths.browser = join(outDir, `${outBaseName}.browser.js`);
869
975
  if (single) {
870
976
  const absFile = files[0];
871
977
  const sourceContent = readFileSync(absFile, 'utf-8');
@@ -1116,10 +1222,10 @@ async function devServer(args) {
1116
1222
  writeFileSync(join(outDir, `${outBaseName}.shared.js`), output.shared);
1117
1223
  }
1118
1224
 
1119
- if (output.client) {
1120
- const p = join(outDir, `${outBaseName}.client.js`);
1121
- writeFileSync(p, output.client);
1122
- clientHTML = await generateDevHTML(output.client, srcDir, actualReloadPort);
1225
+ if (output.browser) {
1226
+ const p = join(outDir, `${outBaseName}.browser.js`);
1227
+ writeFileSync(p, output.browser);
1228
+ clientHTML = await generateDevHTML(output.browser, srcDir, actualReloadPort);
1123
1229
  writeFileSync(join(outDir, 'index.html'), clientHTML);
1124
1230
  hasClient = true;
1125
1231
  }
@@ -1150,10 +1256,10 @@ async function devServer(args) {
1150
1256
  }
1151
1257
  }
1152
1258
 
1153
- if (output.multiBlock && output.clients) {
1154
- for (const [name, code] of Object.entries(output.clients)) {
1259
+ if (output.multiBlock && output.browsers) {
1260
+ for (const [name, code] of Object.entries(output.browsers)) {
1155
1261
  if (name === 'default') continue;
1156
- const p = join(outDir, `${outBaseName}.client.${name}.js`);
1262
+ const p = join(outDir, `${outBaseName}.browser.${name}.js`);
1157
1263
  writeFileSync(p, code);
1158
1264
  }
1159
1265
  }
@@ -1264,9 +1370,9 @@ async function devServer(args) {
1264
1370
  if (output.shared && output.shared.trim()) {
1265
1371
  writeFileSync(join(outDir, `${outBaseName}.shared.js`), output.shared);
1266
1372
  }
1267
- if (output.client) {
1268
- writeFileSync(join(outDir, `${outBaseName}.client.js`), output.client);
1269
- rebuildClientHTML = await generateDevHTML(output.client, srcDir, actualReloadPort);
1373
+ if (output.browser) {
1374
+ writeFileSync(join(outDir, `${outBaseName}.browser.js`), output.browser);
1375
+ rebuildClientHTML = await generateDevHTML(output.browser, srcDir, actualReloadPort);
1270
1376
  writeFileSync(join(outDir, 'index.html'), rebuildClientHTML);
1271
1377
  }
1272
1378
  if (output.server) {
@@ -1895,6 +2001,62 @@ async function installDeps() {
1895
2001
  return;
1896
2002
  }
1897
2003
 
2004
+ // Resolve Tova module dependencies (if any)
2005
+ const tovaDeps = config.dependencies || {};
2006
+ const { isTovModule: _isTovMod } = await import('../src/config/module-path.js');
2007
+ const tovModuleKeys = Object.keys(tovaDeps).filter(k => _isTovMod(k));
2008
+
2009
+ if (tovModuleKeys.length > 0) {
2010
+ const { resolveDependencies } = await import('../src/config/resolver.js');
2011
+ const { listRemoteTags, fetchModule, getCommitSha } = await import('../src/config/git-resolver.js');
2012
+ const { isVersionCached, getModuleCachePath } = await import('../src/config/module-cache.js');
2013
+ const { readLockFile, writeLockFile } = await import('../src/config/lock-file.js');
2014
+
2015
+ console.log(' Resolving Tova dependencies...');
2016
+
2017
+ const lock = readLockFile(cwd);
2018
+ const tovaModuleDeps = {};
2019
+ for (const k of tovModuleKeys) {
2020
+ tovaModuleDeps[k] = tovaDeps[k];
2021
+ }
2022
+
2023
+ try {
2024
+ const { resolved, npmDeps } = await resolveDependencies(tovaModuleDeps, {
2025
+ getAvailableVersions: async (mod) => {
2026
+ if (lock?.modules?.[mod]) return [lock.modules[mod].version];
2027
+ const tags = await listRemoteTags(mod);
2028
+ return tags.map(t => t.version);
2029
+ },
2030
+ getModuleConfig: async (mod, version) => {
2031
+ if (!isVersionCached(mod, version)) {
2032
+ console.log(` Fetching ${mod}@v${version}...`);
2033
+ await fetchModule(mod, version);
2034
+ }
2035
+ const modPath = getModuleCachePath(mod, version);
2036
+ try {
2037
+ return resolveConfig(modPath);
2038
+ } catch { return null; }
2039
+ },
2040
+ getVersionSha: async (mod, version) => {
2041
+ if (lock?.modules?.[mod]?.sha) return lock.modules[mod].sha;
2042
+ return await getCommitSha(mod, version);
2043
+ },
2044
+ });
2045
+
2046
+ writeLockFile(cwd, resolved, npmDeps);
2047
+ console.log(` Resolved ${Object.keys(resolved).length} Tova module(s)`);
2048
+
2049
+ // Merge transitive npm deps into config for package.json generation
2050
+ if (Object.keys(npmDeps).length > 0) {
2051
+ if (!config.npm) config.npm = {};
2052
+ if (!config.npm.prod) config.npm.prod = {};
2053
+ Object.assign(config.npm.prod, npmDeps);
2054
+ }
2055
+ } catch (err) {
2056
+ console.error(` Failed to resolve Tova dependencies: ${err.message}`);
2057
+ }
2058
+ }
2059
+
1898
2060
  // Generate shadow package.json from tova.toml
1899
2061
  const wrote = writePackageJson(config, cwd);
1900
2062
  if (wrote) {
@@ -1903,7 +2065,9 @@ async function installDeps() {
1903
2065
  const code = await new Promise(res => proc.on('close', res));
1904
2066
  process.exit(code);
1905
2067
  } else {
1906
- console.log(' No npm dependencies in tova.toml. Nothing to install.\n');
2068
+ if (tovModuleKeys.length === 0) {
2069
+ console.log(' No npm dependencies in tova.toml. Nothing to install.\n');
2070
+ }
1907
2071
  }
1908
2072
  }
1909
2073
 
@@ -1968,21 +2132,56 @@ async function addDep(args) {
1968
2132
  await installDeps();
1969
2133
  } else {
1970
2134
  // Tova native dependency
1971
- let name = actualPkg;
1972
- let source = actualPkg;
2135
+ const { isTovModule: isTovMod } = await import('../src/config/module-path.js');
2136
+
2137
+ // Parse potential @version suffix
2138
+ let pkgName = actualPkg;
2139
+ let versionConstraint = null;
2140
+ if (pkgName.includes('@') && !pkgName.startsWith('@')) {
2141
+ const atIdx = pkgName.lastIndexOf('@');
2142
+ versionConstraint = pkgName.slice(atIdx + 1);
2143
+ pkgName = pkgName.slice(0, atIdx);
2144
+ }
2145
+
2146
+ if (isTovMod(pkgName)) {
2147
+ // Tova module: fetch tags, pick version, add to [dependencies]
2148
+ const { listRemoteTags, pickLatestTag } = await import('../src/config/git-resolver.js');
2149
+ try {
2150
+ const tags = await listRemoteTags(pkgName);
2151
+ if (tags.length === 0) {
2152
+ console.error(` No version tags found for ${pkgName}`);
2153
+ process.exit(1);
2154
+ }
2155
+ if (!versionConstraint) {
2156
+ const latest = pickLatestTag(tags);
2157
+ versionConstraint = `^${latest.version}`;
2158
+ }
2159
+ addToSection(tomlPath, 'dependencies', `"${pkgName}"`, versionConstraint);
2160
+ console.log(` Added ${pkgName}@${versionConstraint} to [dependencies] in tova.toml`);
2161
+ await installDeps();
2162
+ } catch (err) {
2163
+ console.error(` Failed to add ${pkgName}: ${err.message}`);
2164
+ process.exit(1);
2165
+ }
2166
+ return;
2167
+ }
2168
+
2169
+ // Local path or generic dependency
2170
+ let name = pkgName;
2171
+ let source = pkgName;
1973
2172
 
1974
2173
  // Detect source type
1975
- if (actualPkg.startsWith('file:') || actualPkg.startsWith('./') || actualPkg.startsWith('../') || actualPkg.startsWith('/')) {
2174
+ if (pkgName.startsWith('file:') || pkgName.startsWith('./') || pkgName.startsWith('../') || pkgName.startsWith('/')) {
1976
2175
  // Local path dependency
1977
- source = actualPkg.startsWith('file:') ? actualPkg : `file:${actualPkg}`;
1978
- name = basename(actualPkg.replace(/^file:/, ''));
1979
- } else if (actualPkg.startsWith('git:') || actualPkg.includes('github.com/') || actualPkg.includes('.git')) {
2176
+ source = pkgName.startsWith('file:') ? pkgName : `file:${pkgName}`;
2177
+ name = basename(pkgName.replace(/^file:/, ''));
2178
+ } else if (pkgName.startsWith('git:') || pkgName.includes('.git')) {
1980
2179
  // Git dependency
1981
- source = actualPkg.startsWith('git:') ? actualPkg : `git:${actualPkg}`;
1982
- name = basename(actualPkg.replace(/\.git$/, '').replace(/^git:/, ''));
2180
+ source = pkgName.startsWith('git:') ? pkgName : `git:${pkgName}`;
2181
+ name = basename(pkgName.replace(/\.git$/, '').replace(/^git:/, ''));
1983
2182
  } else {
1984
2183
  // Tova registry package (future: for now, just store the name)
1985
- source = `*`;
2184
+ source = versionConstraint || `*`;
1986
2185
  }
1987
2186
 
1988
2187
  addToSection(tomlPath, 'dependencies', name, source);
@@ -2540,7 +2739,7 @@ async function startRepl() {
2540
2739
  'in', 'return', 'match', 'type', 'import', 'from', 'and', 'or', 'not',
2541
2740
  'try', 'catch', 'finally', 'break', 'continue', 'async', 'await',
2542
2741
  'guard', 'interface', 'derive', 'pub', 'impl', 'trait', 'defer',
2543
- 'yield', 'extern', 'is', 'with', 'as', 'export', 'server', 'client', 'shared',
2742
+ 'yield', 'extern', 'is', 'with', 'as', 'export', 'server', 'client', 'browser', 'shared',
2544
2743
  ]);
2545
2744
 
2546
2745
  const TYPE_NAMES = new Set([
@@ -3040,7 +3239,7 @@ async function binaryBuild(srcDir, outputName, outDir) {
3040
3239
  // Step 1: Compile all .tova files to JS
3041
3240
  const sharedParts = [];
3042
3241
  const serverParts = [];
3043
- const clientParts = [];
3242
+ const browserParts = [];
3044
3243
 
3045
3244
  for (const file of tovaFiles) {
3046
3245
  try {
@@ -3048,7 +3247,7 @@ async function binaryBuild(srcDir, outputName, outDir) {
3048
3247
  const output = compileTova(source, file);
3049
3248
  if (output.shared) sharedParts.push(output.shared);
3050
3249
  if (output.server) serverParts.push(output.server);
3051
- if (output.client) clientParts.push(output.client);
3250
+ if (output.browser) browserParts.push(output.browser);
3052
3251
  } catch (err) {
3053
3252
  console.error(` Error in ${relative(srcDir, file)}: ${err.message}`);
3054
3253
  process.exit(1);
@@ -3118,7 +3317,7 @@ async function productionBuild(srcDir, outDir) {
3118
3317
 
3119
3318
  console.log(`\n Production build...\n`);
3120
3319
 
3121
- const clientParts = [];
3320
+ const browserParts = [];
3122
3321
  const serverParts = [];
3123
3322
  const sharedParts = [];
3124
3323
  let cssContent = '';
@@ -3130,14 +3329,14 @@ async function productionBuild(srcDir, outDir) {
3130
3329
 
3131
3330
  if (output.shared) sharedParts.push(output.shared);
3132
3331
  if (output.server) serverParts.push(output.server);
3133
- if (output.client) clientParts.push(output.client);
3332
+ if (output.browser) browserParts.push(output.browser);
3134
3333
  } catch (err) {
3135
3334
  console.error(` Error in ${relative(srcDir, file)}: ${err.message}`);
3136
3335
  process.exit(1);
3137
3336
  }
3138
3337
  }
3139
3338
 
3140
- const allClientCode = clientParts.join('\n');
3339
+ const allClientCode = browserParts.join('\n');
3141
3340
  const allServerCode = serverParts.join('\n');
3142
3341
  const allSharedCode = sharedParts.join('\n');
3143
3342
 
@@ -3485,7 +3684,8 @@ class BuildCache {
3485
3684
  }
3486
3685
 
3487
3686
  _hashContent(content) {
3488
- return Bun.hash(content).toString(16);
3687
+ if (typeof Bun !== 'undefined' && Bun.hash) return Bun.hash(content).toString(16);
3688
+ return _cryptoHash('md5').update(content).digest('hex');
3489
3689
  }
3490
3690
 
3491
3691
  load() {
@@ -3526,7 +3726,8 @@ class BuildCache {
3526
3726
  for (const f of files.slice().sort()) {
3527
3727
  combined += f + readFileSync(f, 'utf-8');
3528
3728
  }
3529
- return Bun.hash(combined).toString(16);
3729
+ if (typeof Bun !== 'undefined' && Bun.hash) return Bun.hash(combined).toString(16);
3730
+ return _cryptoHash('md5').update(combined).digest('hex');
3530
3731
  }
3531
3732
 
3532
3733
  // Store compiled output for a multi-file group
@@ -3598,7 +3799,7 @@ function getCompiledExtension(tovaPath) {
3598
3799
  const lexer = new Lexer(src, tovaPath);
3599
3800
  const tokens = lexer.tokenize();
3600
3801
  // Check if any top-level token is a block keyword (shared/server/client/test/bench/data)
3601
- const BLOCK_KEYWORDS = new Set(['shared', 'server', 'client', 'test', 'bench', 'data']);
3802
+ const BLOCK_KEYWORDS = new Set(['shared', 'server', 'client', 'browser', 'test', 'bench', 'data']);
3602
3803
  let depth = 0;
3603
3804
  for (const tok of tokens) {
3604
3805
  if (tok.type === 'LBRACE') depth++;
@@ -3725,7 +3926,7 @@ function collectExports(ast, filename) {
3725
3926
 
3726
3927
  for (const node of ast.body) {
3727
3928
  // Also collect exports from inside shared/server/client blocks
3728
- if (node.type === 'SharedBlock' || node.type === 'ServerBlock' || node.type === 'ClientBlock') {
3929
+ if (node.type === 'SharedBlock' || node.type === 'ServerBlock' || node.type === 'BrowserBlock') {
3729
3930
  if (node.body) {
3730
3931
  for (const inner of node.body) {
3731
3932
  collectFromNode(inner);
@@ -3755,7 +3956,7 @@ function compileWithImports(source, filename, srcDir) {
3755
3956
  const ast = parser.parse();
3756
3957
 
3757
3958
  // Cache module type from AST (avoids regex heuristic on subsequent lookups)
3758
- const hasBlocks = ast.body.some(n => n.type === 'SharedBlock' || n.type === 'ServerBlock' || n.type === 'ClientBlock' || n.type === 'TestBlock' || n.type === 'BenchBlock' || n.type === 'DataBlock');
3959
+ const hasBlocks = ast.body.some(n => n.type === 'SharedBlock' || n.type === 'ServerBlock' || n.type === 'BrowserBlock' || n.type === 'TestBlock' || n.type === 'BenchBlock' || n.type === 'DataBlock');
3759
3960
  moduleTypeCache.set(filename, hasBlocks ? '.shared.js' : '.js');
3760
3961
 
3761
3962
  // Collect this module's exports for validation
@@ -3853,32 +4054,32 @@ function validateMergedAST(mergedBlocks, sourceFiles) {
3853
4054
  );
3854
4055
  }
3855
4056
 
3856
- // Check client blocks — top-level declarations only
3857
- const clientDecls = { component: new Map(), state: new Map(), computed: new Map(), store: new Map(), fn: new Map() };
3858
- for (const block of mergedBlocks.clientBlocks) {
4057
+ // Check browser blocks — top-level declarations only
4058
+ const browserDecls = { component: new Map(), state: new Map(), computed: new Map(), store: new Map(), fn: new Map() };
4059
+ for (const block of mergedBlocks.browserBlocks) {
3859
4060
  for (const stmt of block.body) {
3860
4061
  const loc = stmt.loc || block.loc;
3861
4062
  if (stmt.type === 'ComponentDeclaration') {
3862
- if (clientDecls.component.has(stmt.name)) addDup('component', stmt.name, clientDecls.component.get(stmt.name), loc);
3863
- else clientDecls.component.set(stmt.name, loc);
4063
+ if (browserDecls.component.has(stmt.name)) addDup('component', stmt.name, browserDecls.component.get(stmt.name), loc);
4064
+ else browserDecls.component.set(stmt.name, loc);
3864
4065
  } else if (stmt.type === 'StateDeclaration') {
3865
4066
  const name = stmt.name || (stmt.targets && stmt.targets[0]);
3866
4067
  if (name) {
3867
- if (clientDecls.state.has(name)) addDup('state', name, clientDecls.state.get(name), loc);
3868
- else clientDecls.state.set(name, loc);
4068
+ if (browserDecls.state.has(name)) addDup('state', name, browserDecls.state.get(name), loc);
4069
+ else browserDecls.state.set(name, loc);
3869
4070
  }
3870
4071
  } else if (stmt.type === 'ComputedDeclaration') {
3871
4072
  const name = stmt.name;
3872
4073
  if (name) {
3873
- if (clientDecls.computed.has(name)) addDup('computed', name, clientDecls.computed.get(name), loc);
3874
- else clientDecls.computed.set(name, loc);
4074
+ if (browserDecls.computed.has(name)) addDup('computed', name, browserDecls.computed.get(name), loc);
4075
+ else browserDecls.computed.set(name, loc);
3875
4076
  }
3876
4077
  } else if (stmt.type === 'StoreDeclaration') {
3877
- if (clientDecls.store.has(stmt.name)) addDup('store', stmt.name, clientDecls.store.get(stmt.name), loc);
3878
- else clientDecls.store.set(stmt.name, loc);
4078
+ if (browserDecls.store.has(stmt.name)) addDup('store', stmt.name, browserDecls.store.get(stmt.name), loc);
4079
+ else browserDecls.store.set(stmt.name, loc);
3879
4080
  } else if (stmt.type === 'FunctionDeclaration') {
3880
- if (clientDecls.fn.has(stmt.name)) addDup('function', stmt.name, clientDecls.fn.get(stmt.name), loc);
3881
- else clientDecls.fn.set(stmt.name, loc);
4081
+ if (browserDecls.fn.has(stmt.name)) addDup('function', stmt.name, browserDecls.fn.get(stmt.name), loc);
4082
+ else browserDecls.fn.set(stmt.name, loc);
3882
4083
  }
3883
4084
  }
3884
4085
  }
@@ -4022,7 +4223,7 @@ function mergeDirectory(dir, srcDir, options = {}) {
4022
4223
  const mergedBody = [];
4023
4224
  const sharedBlocks = [];
4024
4225
  const serverBlocks = [];
4025
- const clientBlocks = [];
4226
+ const browserBlocks = [];
4026
4227
 
4027
4228
  for (const { file, ast } of parsedFiles) {
4028
4229
  for (const node of ast.body) {
@@ -4043,14 +4244,14 @@ function mergeDirectory(dir, srcDir, options = {}) {
4043
4244
 
4044
4245
  if (node.type === 'SharedBlock') sharedBlocks.push(node);
4045
4246
  else if (node.type === 'ServerBlock') serverBlocks.push(node);
4046
- else if (node.type === 'ClientBlock') clientBlocks.push(node);
4247
+ else if (node.type === 'BrowserBlock') browserBlocks.push(node);
4047
4248
 
4048
4249
  mergedBody.push(node);
4049
4250
  }
4050
4251
  }
4051
4252
 
4052
4253
  // Validate for duplicate declarations across files
4053
- validateMergedAST({ sharedBlocks, serverBlocks, clientBlocks }, tovaFiles);
4254
+ validateMergedAST({ sharedBlocks, serverBlocks, browserBlocks }, tovaFiles);
4054
4255
 
4055
4256
  // Build merged Program AST
4056
4257
  const mergedAST = new Program(mergedBody);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.5.1",
3
+ "version": "0.8.2",
4
4
  "description": "Tova — a modern programming language that transpiles to JavaScript, unifying frontend and backend",
5
5
  "type": "module",
6
6
  "main": "src/index.js",