tova 0.7.0 → 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 (38) hide show
  1. package/bin/tova.js +192 -10
  2. package/package.json +2 -7
  3. package/src/analyzer/analyzer.js +134 -2
  4. package/src/analyzer/deploy-analyzer.js +44 -0
  5. package/src/codegen/base-codegen.js +1159 -10
  6. package/src/codegen/codegen.js +20 -0
  7. package/src/codegen/deploy-codegen.js +49 -0
  8. package/src/codegen/shared-codegen.js +5 -0
  9. package/src/codegen/wasm-codegen.js +6 -0
  10. package/src/config/edit-toml.js +6 -2
  11. package/src/config/git-resolver.js +128 -0
  12. package/src/config/lock-file.js +57 -0
  13. package/src/config/module-cache.js +58 -0
  14. package/src/config/module-entry.js +37 -0
  15. package/src/config/module-path.js +31 -0
  16. package/src/config/pkg-errors.js +62 -0
  17. package/src/config/resolve.js +17 -0
  18. package/src/config/resolver.js +139 -0
  19. package/src/config/search.js +28 -0
  20. package/src/config/semver.js +72 -0
  21. package/src/config/toml.js +48 -5
  22. package/src/deploy/deploy.js +217 -0
  23. package/src/deploy/infer.js +218 -0
  24. package/src/deploy/provision.js +311 -0
  25. package/src/lsp/server.js +482 -0
  26. package/src/parser/ast.js +24 -0
  27. package/src/parser/concurrency-ast.js +15 -0
  28. package/src/parser/concurrency-parser.js +236 -0
  29. package/src/parser/deploy-ast.js +37 -0
  30. package/src/parser/deploy-parser.js +132 -0
  31. package/src/parser/parser.js +21 -3
  32. package/src/parser/select-ast.js +39 -0
  33. package/src/registry/plugins/concurrency-plugin.js +32 -0
  34. package/src/registry/plugins/deploy-plugin.js +33 -0
  35. package/src/registry/register-all.js +4 -0
  36. package/src/stdlib/inline.js +35 -3
  37. package/src/stdlib/runtime-bridge.js +152 -0
  38. package/src/version.js +1 -1
package/bin/tova.js CHANGED
@@ -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 = {}) {
@@ -1912,6 +2001,62 @@ async function installDeps() {
1912
2001
  return;
1913
2002
  }
1914
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
+
1915
2060
  // Generate shadow package.json from tova.toml
1916
2061
  const wrote = writePackageJson(config, cwd);
1917
2062
  if (wrote) {
@@ -1920,7 +2065,9 @@ async function installDeps() {
1920
2065
  const code = await new Promise(res => proc.on('close', res));
1921
2066
  process.exit(code);
1922
2067
  } else {
1923
- 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
+ }
1924
2071
  }
1925
2072
  }
1926
2073
 
@@ -1985,21 +2132,56 @@ async function addDep(args) {
1985
2132
  await installDeps();
1986
2133
  } else {
1987
2134
  // Tova native dependency
1988
- let name = actualPkg;
1989
- 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;
1990
2172
 
1991
2173
  // Detect source type
1992
- if (actualPkg.startsWith('file:') || actualPkg.startsWith('./') || actualPkg.startsWith('../') || actualPkg.startsWith('/')) {
2174
+ if (pkgName.startsWith('file:') || pkgName.startsWith('./') || pkgName.startsWith('../') || pkgName.startsWith('/')) {
1993
2175
  // Local path dependency
1994
- source = actualPkg.startsWith('file:') ? actualPkg : `file:${actualPkg}`;
1995
- name = basename(actualPkg.replace(/^file:/, ''));
1996
- } 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')) {
1997
2179
  // Git dependency
1998
- source = actualPkg.startsWith('git:') ? actualPkg : `git:${actualPkg}`;
1999
- name = basename(actualPkg.replace(/\.git$/, '').replace(/^git:/, ''));
2180
+ source = pkgName.startsWith('git:') ? pkgName : `git:${pkgName}`;
2181
+ name = basename(pkgName.replace(/\.git$/, '').replace(/^git:/, ''));
2000
2182
  } else {
2001
2183
  // Tova registry package (future: for now, just store the name)
2002
- source = `*`;
2184
+ source = versionConstraint || `*`;
2003
2185
  }
2004
2186
 
2005
2187
  addToSection(tomlPath, 'dependencies', name, source);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.7.0",
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",
@@ -36,12 +36,7 @@
36
36
  "url": "https://github.com/tova-lang/tova-lang/issues"
37
37
  },
38
38
  "author": "Enoch Kujem Abassey",
39
- "keywords": [
40
- "language",
41
- "transpiler",
42
- "fullstack",
43
- "javascript"
44
- ],
39
+ "keywords": ["language", "transpiler", "fullstack", "javascript"],
45
40
  "license": "MIT",
46
41
  "devDependencies": {
47
42
  "@codemirror/autocomplete": "^6.20.0",
@@ -900,6 +900,19 @@ export class Analyzer {
900
900
  }
901
901
  this.visitExpression(node.argument);
902
902
  return;
903
+ case 'SpawnExpression':
904
+ if (!this._concurrentDepth) {
905
+ this.warn("'spawn' should be used inside a 'concurrent' block", node.loc, null, {
906
+ code: 'W_SPAWN_OUTSIDE_CONCURRENT',
907
+ });
908
+ }
909
+ if (node.callee) this.visitExpression(node.callee);
910
+ if (node.arguments) {
911
+ for (const arg of node.arguments) {
912
+ this.visitExpression(arg);
913
+ }
914
+ }
915
+ return;
903
916
  case 'YieldExpression':
904
917
  if (node.argument) this.visitExpression(node.argument);
905
918
  return;
@@ -1019,6 +1032,123 @@ export class Analyzer {
1019
1032
  }
1020
1033
  }
1021
1034
 
1035
+ visitConcurrentBlock(node) {
1036
+ // Validate mode
1037
+ const validModes = new Set(['all', 'cancel_on_error', 'first', 'timeout']);
1038
+ if (!validModes.has(node.mode)) {
1039
+ this.warn(`Unknown concurrent block mode '${node.mode}'`, node.loc, null, {
1040
+ code: 'W_UNKNOWN_CONCURRENT_MODE',
1041
+ });
1042
+ }
1043
+
1044
+ // Validate timeout
1045
+ if (node.mode === 'timeout' && !node.timeout) {
1046
+ this.warn("concurrent timeout mode requires a timeout value", node.loc, null, {
1047
+ code: 'W_MISSING_TIMEOUT',
1048
+ });
1049
+ }
1050
+
1051
+ // Warn on empty block
1052
+ if (node.body.length === 0) {
1053
+ this.warn("Empty concurrent block", node.loc, null, {
1054
+ code: 'W_EMPTY_CONCURRENT',
1055
+ });
1056
+ }
1057
+
1058
+ // Track concurrent depth for spawn validation
1059
+ this._concurrentDepth = (this._concurrentDepth || 0) + 1;
1060
+
1061
+ // Visit body statements (concurrent block does NOT create a new scope —
1062
+ // variables assigned inside should be visible after the block)
1063
+ for (const stmt of node.body) {
1064
+ this.visitNode(stmt);
1065
+ }
1066
+
1067
+ // Check spawned functions for WASM compatibility — warn if mixed WASM/non-WASM
1068
+ let hasWasm = false;
1069
+ let hasNonWasm = false;
1070
+ for (const stmt of node.body) {
1071
+ const spawn = (stmt.type === 'Assignment' && stmt.values && stmt.values[0] && stmt.values[0].type === 'SpawnExpression')
1072
+ ? stmt.values[0]
1073
+ : (stmt.type === 'ExpressionStatement' && stmt.expression && stmt.expression.type === 'SpawnExpression')
1074
+ ? stmt.expression
1075
+ : null;
1076
+ if (!spawn) continue;
1077
+ const calleeName = spawn.callee && spawn.callee.type === 'Identifier' ? spawn.callee.name : null;
1078
+ if (calleeName) {
1079
+ const sym = this.currentScope.lookup(calleeName);
1080
+ if (sym && sym.isWasm) {
1081
+ hasWasm = true;
1082
+ } else {
1083
+ hasNonWasm = true;
1084
+ }
1085
+ } else {
1086
+ // Lambda or complex expression — always non-WASM
1087
+ hasNonWasm = true;
1088
+ }
1089
+ }
1090
+ if (hasWasm && hasNonWasm) {
1091
+ this.warn(
1092
+ "concurrent block mixes @wasm and non-WASM tasks — non-WASM tasks will fall back to async JS execution",
1093
+ node.loc, null, { code: 'W_SPAWN_WASM_FALLBACK' }
1094
+ );
1095
+ }
1096
+
1097
+ this._concurrentDepth--;
1098
+ }
1099
+
1100
+ visitSelectStatement(node) {
1101
+ if (node.cases.length === 0) {
1102
+ this.warn("Empty select block", node.loc, null, {
1103
+ code: 'W_EMPTY_SELECT',
1104
+ });
1105
+ }
1106
+
1107
+ let defaultCount = 0;
1108
+ let timeoutCount = 0;
1109
+ for (const c of node.cases) {
1110
+ if (c.kind === 'default') defaultCount++;
1111
+ if (c.kind === 'timeout') timeoutCount++;
1112
+ }
1113
+
1114
+ if (defaultCount > 1) {
1115
+ this.warn("select block has multiple default cases", node.loc, null, {
1116
+ code: 'W_DUPLICATE_SELECT_DEFAULT',
1117
+ });
1118
+ }
1119
+ if (timeoutCount > 1) {
1120
+ this.warn("select block has multiple timeout cases", node.loc, null, {
1121
+ code: 'W_DUPLICATE_SELECT_TIMEOUT',
1122
+ });
1123
+ }
1124
+ if (defaultCount > 0 && timeoutCount > 0) {
1125
+ this.warn("select block has both default and timeout — default makes timeout unreachable", node.loc, null, {
1126
+ code: 'W_SELECT_DEFAULT_TIMEOUT',
1127
+ });
1128
+ }
1129
+
1130
+ // Visit each case's expressions and body
1131
+ for (const c of node.cases) {
1132
+ if (c.channel) this.visitNode(c.channel);
1133
+ if (c.value) this.visitNode(c.value);
1134
+
1135
+ if (c.kind === 'receive' && c.binding) {
1136
+ // Create scope for the binding variable
1137
+ this.pushScope('select-case');
1138
+ this.currentScope.define(c.binding,
1139
+ new Symbol(c.binding, 'variable', null, false, c.loc));
1140
+ for (const stmt of c.body) {
1141
+ this.visitNode(stmt);
1142
+ }
1143
+ this.popScope();
1144
+ } else {
1145
+ for (const stmt of c.body) {
1146
+ this.visitNode(stmt);
1147
+ }
1148
+ }
1149
+ }
1150
+ }
1151
+
1022
1152
  _validateCliCrossBlock() {
1023
1153
  const cliBlocks = this.ast.body.filter(n => n.type === 'CliBlock');
1024
1154
  if (cliBlocks.length === 0) return;
@@ -1614,9 +1744,10 @@ export class Analyzer {
1614
1744
  } else if (node.pattern.type === 'ArrayPattern' || node.pattern.type === 'TuplePattern') {
1615
1745
  for (const el of node.pattern.elements) {
1616
1746
  if (el) {
1747
+ const varName = el.startsWith('...') ? el.slice(3) : el;
1617
1748
  try {
1618
- this.currentScope.define(el,
1619
- new Symbol(el, 'variable', null, false, node.loc));
1749
+ this.currentScope.define(varName,
1750
+ new Symbol(varName, 'variable', null, false, node.loc));
1620
1751
  } catch (e) {
1621
1752
  this.error(e.message);
1622
1753
  }
@@ -1634,6 +1765,7 @@ export class Analyzer {
1634
1765
  sym._paramTypes = node.params.map(p => p.typeAnnotation || null);
1635
1766
  sym._typeParams = node.typeParams || [];
1636
1767
  sym.isPublic = node.isPublic || false;
1768
+ sym.isWasm = !!(node.decorators && node.decorators.some(d => d.name === 'wasm'));
1637
1769
  this.currentScope.define(node.name, sym);
1638
1770
  } catch (e) {
1639
1771
  this.error(e.message);
@@ -0,0 +1,44 @@
1
+ // Deploy-specific analyzer methods for the Tova language
2
+ // Extracted from analyzer.js for lazy loading — only loaded when deploy { } blocks are encountered.
3
+
4
+ const KNOWN_DEPLOY_FIELDS = new Set([
5
+ 'server', 'domain', 'instances', 'memory', 'branch',
6
+ 'health', 'health_interval', 'health_timeout',
7
+ 'restart_on_failure', 'keep_releases',
8
+ ]);
9
+
10
+ const REQUIRED_DEPLOY_FIELDS = ['server', 'domain'];
11
+
12
+ export function installDeployAnalyzer(AnalyzerClass) {
13
+ if (AnalyzerClass.prototype._deployAnalyzerInstalled) return;
14
+ AnalyzerClass.prototype._deployAnalyzerInstalled = true;
15
+
16
+ AnalyzerClass.prototype.visitDeployBlock = function(node) {
17
+ // Collect config field keys present in the deploy block body
18
+ const presentFields = new Set();
19
+ for (const stmt of node.body) {
20
+ if (stmt.type === 'DeployConfigField') {
21
+ // Validate unknown fields
22
+ if (!KNOWN_DEPLOY_FIELDS.has(stmt.key)) {
23
+ this.error(
24
+ `Unknown deploy config field "${stmt.key}"`,
25
+ stmt.loc,
26
+ `Known fields: ${[...KNOWN_DEPLOY_FIELDS].join(', ')}`
27
+ );
28
+ }
29
+ presentFields.add(stmt.key);
30
+ }
31
+ // DeployEnvBlock and DeployDbBlock are valid sub-blocks — no additional validation needed
32
+ }
33
+
34
+ // Validate required fields
35
+ for (const required of REQUIRED_DEPLOY_FIELDS) {
36
+ if (!presentFields.has(required)) {
37
+ this.error(
38
+ `Deploy block "${node.name}" is missing required field "${required}"`,
39
+ node.loc
40
+ );
41
+ }
42
+ }
43
+ };
44
+ }