tova 0.3.9 → 0.4.1
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 +80 -57
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +14 -5
- package/src/analyzer/client-analyzer.js +20 -23
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { resolve, basename, dirname, join, relative } from 'path';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync, rmSync, watch as fsWatch } from 'fs';
|
|
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
6
|
// Bun.hash used instead of crypto.createHash for faster hashing
|
|
7
7
|
import { Lexer } from '../src/lexer/lexer.js';
|
|
@@ -1443,7 +1443,7 @@ shared {
|
|
|
1443
1443
|
}
|
|
1444
1444
|
|
|
1445
1445
|
server {
|
|
1446
|
-
fn get_message()
|
|
1446
|
+
fn get_message() {
|
|
1447
1447
|
Message("Hello from Tova!", Date.new().toLocaleTimeString())
|
|
1448
1448
|
}
|
|
1449
1449
|
|
|
@@ -1560,7 +1560,7 @@ client {
|
|
|
1560
1560
|
content: name => `// ${name} — Built with Tova
|
|
1561
1561
|
|
|
1562
1562
|
server {
|
|
1563
|
-
fn health()
|
|
1563
|
+
fn health() {
|
|
1564
1564
|
{ status: "ok" }
|
|
1565
1565
|
}
|
|
1566
1566
|
|
|
@@ -4397,6 +4397,17 @@ function detectInstallMethod() {
|
|
|
4397
4397
|
return 'npm';
|
|
4398
4398
|
}
|
|
4399
4399
|
|
|
4400
|
+
function compareSemver(a, b) {
|
|
4401
|
+
// Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
4402
|
+
const pa = a.split('.').map(Number);
|
|
4403
|
+
const pb = b.split('.').map(Number);
|
|
4404
|
+
for (let i = 0; i < 3; i++) {
|
|
4405
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
4406
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
4407
|
+
}
|
|
4408
|
+
return 0;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4400
4411
|
async function upgradeCommand() {
|
|
4401
4412
|
console.log(`\n Current version: ${color.bold('Tova v' + VERSION)}\n`);
|
|
4402
4413
|
console.log(' Checking for updates...');
|
|
@@ -4404,102 +4415,95 @@ async function upgradeCommand() {
|
|
|
4404
4415
|
const installMethod = detectInstallMethod();
|
|
4405
4416
|
|
|
4406
4417
|
try {
|
|
4418
|
+
// Always check npm registry as the source of truth for latest version
|
|
4419
|
+
const res = await fetch('https://registry.npmjs.org/tova/latest');
|
|
4420
|
+
if (!res.ok) {
|
|
4421
|
+
console.error(color.red(' Could not reach the npm registry. Check your network connection.'));
|
|
4422
|
+
process.exit(1);
|
|
4423
|
+
}
|
|
4424
|
+
const data = await res.json();
|
|
4425
|
+
const latestVersion = data.version;
|
|
4426
|
+
|
|
4427
|
+
if (compareSemver(VERSION, latestVersion) >= 0) {
|
|
4428
|
+
console.log(` ${color.green('Already on the latest version')} (v${VERSION}).\n`);
|
|
4429
|
+
return;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
console.log(` New version available: ${color.green('v' + latestVersion)}\n`);
|
|
4433
|
+
|
|
4407
4434
|
if (installMethod === 'binary') {
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4435
|
+
console.log(' Upgrading via binary...');
|
|
4436
|
+
|
|
4437
|
+
// Check GitHub releases for the matching binary
|
|
4438
|
+
const ghRes = await fetch('https://api.github.com/repos/tova-lang/tova-lang/releases/latest');
|
|
4439
|
+
let ghTag = null;
|
|
4440
|
+
if (ghRes.ok) {
|
|
4441
|
+
const ghData = await ghRes.json();
|
|
4442
|
+
const ghVersion = (ghData.tag_name || '').replace(/^v/, '');
|
|
4443
|
+
if (compareSemver(ghVersion, VERSION) > 0) {
|
|
4444
|
+
ghTag = ghData.tag_name;
|
|
4445
|
+
}
|
|
4413
4446
|
}
|
|
4414
|
-
const data = await res.json();
|
|
4415
|
-
const latestVersion = (data.tag_name || '').replace(/^v/, '');
|
|
4416
4447
|
|
|
4417
|
-
if (
|
|
4418
|
-
|
|
4448
|
+
if (!ghTag) {
|
|
4449
|
+
// No newer binary release available — fall back to npm install
|
|
4450
|
+
console.log(` ${color.dim('No binary release for v' + latestVersion + ' yet. Falling back to npm...')}\n`);
|
|
4451
|
+
await npmUpgrade(latestVersion);
|
|
4419
4452
|
return;
|
|
4420
4453
|
}
|
|
4421
4454
|
|
|
4422
|
-
console.log(` New version available: ${color.green('v' + latestVersion)}\n`);
|
|
4423
|
-
console.log(' Upgrading via binary...');
|
|
4424
|
-
|
|
4425
4455
|
// Detect platform
|
|
4426
4456
|
const platform = process.platform === 'darwin' ? 'darwin' : process.platform === 'linux' ? 'linux' : 'windows';
|
|
4427
4457
|
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
4428
4458
|
const assetName = `tova-${platform}-${arch}`;
|
|
4429
|
-
const downloadUrl = `https://github.com/tova-lang/tova-lang/releases/download/${
|
|
4459
|
+
const downloadUrl = `https://github.com/tova-lang/tova-lang/releases/download/${ghTag}/${assetName}.gz`;
|
|
4430
4460
|
|
|
4431
4461
|
const installDir = join(process.env.HOME || '', '.tova', 'bin');
|
|
4432
4462
|
const tmpPath = join(installDir, 'tova.download');
|
|
4433
4463
|
const binPath = join(installDir, 'tova');
|
|
4434
4464
|
|
|
4465
|
+
// Ensure install directory exists
|
|
4466
|
+
mkdirSync(installDir, { recursive: true });
|
|
4467
|
+
|
|
4435
4468
|
// Download compressed binary
|
|
4436
4469
|
const dlRes = await fetch(downloadUrl);
|
|
4437
4470
|
if (!dlRes.ok) {
|
|
4438
4471
|
// Fall back to uncompressed
|
|
4439
4472
|
const dlRes2 = await fetch(downloadUrl.replace('.gz', ''));
|
|
4440
4473
|
if (!dlRes2.ok) {
|
|
4441
|
-
console.
|
|
4442
|
-
|
|
4474
|
+
console.log(` ${color.dim('Binary download failed. Falling back to npm...')}\n`);
|
|
4475
|
+
await npmUpgrade(latestVersion);
|
|
4476
|
+
return;
|
|
4443
4477
|
}
|
|
4444
4478
|
writeFileSync(tmpPath, new Uint8Array(await dlRes2.arrayBuffer()));
|
|
4445
4479
|
} else {
|
|
4446
4480
|
// Decompress gzip
|
|
4447
4481
|
const compressed = new Uint8Array(await dlRes.arrayBuffer());
|
|
4448
|
-
const
|
|
4482
|
+
const { gunzipSync } = await import('zlib');
|
|
4483
|
+
const decompressed = gunzipSync(compressed);
|
|
4449
4484
|
writeFileSync(tmpPath, decompressed);
|
|
4450
4485
|
}
|
|
4451
4486
|
|
|
4452
4487
|
// Make executable
|
|
4453
|
-
const { chmodSync } = await import('fs');
|
|
4454
4488
|
chmodSync(tmpPath, 0o755);
|
|
4455
4489
|
|
|
4456
4490
|
// Verify the new binary works
|
|
4457
|
-
const
|
|
4458
|
-
|
|
4491
|
+
const { spawnSync } = await import('child_process');
|
|
4492
|
+
const verifyProc = spawnSync(tmpPath, ['--version'], { timeout: 10000 });
|
|
4493
|
+
if (verifyProc.status !== 0) {
|
|
4459
4494
|
rmSync(tmpPath, { force: true });
|
|
4460
|
-
console.error(color.red(' Downloaded binary verification failed.'));
|
|
4461
|
-
|
|
4495
|
+
console.error(color.red(' Downloaded binary verification failed. Falling back to npm...'));
|
|
4496
|
+
await npmUpgrade(latestVersion);
|
|
4497
|
+
return;
|
|
4462
4498
|
}
|
|
4463
4499
|
|
|
4464
4500
|
// Atomic rename
|
|
4465
|
-
const { renameSync } = await import('fs');
|
|
4466
4501
|
renameSync(tmpPath, binPath);
|
|
4467
4502
|
|
|
4468
4503
|
console.log(`\n ${color.green('✓')} Upgraded: v${VERSION} -> ${color.bold('v' + latestVersion)}\n`);
|
|
4469
4504
|
} else {
|
|
4470
|
-
// npm/bun install: check npm registry
|
|
4471
|
-
const res = await fetch('https://registry.npmjs.org/tova/latest');
|
|
4472
|
-
if (!res.ok) {
|
|
4473
|
-
console.error(color.red(' Could not reach the npm registry. Check your network connection.'));
|
|
4474
|
-
process.exit(1);
|
|
4475
|
-
}
|
|
4476
|
-
const data = await res.json();
|
|
4477
|
-
const latestVersion = data.version;
|
|
4478
|
-
|
|
4479
|
-
if (latestVersion === VERSION) {
|
|
4480
|
-
console.log(` ${color.green('Already on the latest version')} (v${VERSION}).\n`);
|
|
4481
|
-
return;
|
|
4482
|
-
}
|
|
4483
|
-
|
|
4484
|
-
console.log(` New version available: ${color.green('v' + latestVersion)}\n`);
|
|
4485
4505
|
console.log(' Upgrading...');
|
|
4486
|
-
|
|
4487
|
-
const pm = detectPackageManager();
|
|
4488
|
-
const installCmd = pm === 'bun' ? ['bun', ['add', '-g', 'tova@latest']]
|
|
4489
|
-
: pm === 'pnpm' ? ['pnpm', ['add', '-g', 'tova@latest']]
|
|
4490
|
-
: pm === 'yarn' ? ['yarn', ['global', 'add', 'tova@latest']]
|
|
4491
|
-
: ['npm', ['install', '-g', 'tova@latest']];
|
|
4492
|
-
|
|
4493
|
-
const proc = spawn(installCmd[0], installCmd[1], { stdio: 'inherit' });
|
|
4494
|
-
const exitCode = await new Promise(res => proc.on('close', res));
|
|
4495
|
-
|
|
4496
|
-
if (exitCode === 0) {
|
|
4497
|
-
console.log(`\n ${color.green('✓')} Upgraded to Tova v${latestVersion}.\n`);
|
|
4498
|
-
} else {
|
|
4499
|
-
console.error(color.red(`\n Upgrade failed (exit code ${exitCode}).`));
|
|
4500
|
-
console.error(` Try manually: ${installCmd[0]} ${installCmd[1].join(' ')}\n`);
|
|
4501
|
-
process.exit(1);
|
|
4502
|
-
}
|
|
4506
|
+
await npmUpgrade(latestVersion);
|
|
4503
4507
|
}
|
|
4504
4508
|
} catch (err) {
|
|
4505
4509
|
console.error(color.red(` Upgrade failed: ${err.message}`));
|
|
@@ -4512,6 +4516,25 @@ async function upgradeCommand() {
|
|
|
4512
4516
|
}
|
|
4513
4517
|
}
|
|
4514
4518
|
|
|
4519
|
+
async function npmUpgrade(latestVersion) {
|
|
4520
|
+
const pm = detectPackageManager();
|
|
4521
|
+
const installCmd = pm === 'bun' ? ['bun', ['add', '-g', 'tova@latest']]
|
|
4522
|
+
: pm === 'pnpm' ? ['pnpm', ['add', '-g', 'tova@latest']]
|
|
4523
|
+
: pm === 'yarn' ? ['yarn', ['global', 'add', 'tova@latest']]
|
|
4524
|
+
: ['npm', ['install', '-g', 'tova@latest']];
|
|
4525
|
+
|
|
4526
|
+
const proc = spawn(installCmd[0], installCmd[1], { stdio: 'inherit' });
|
|
4527
|
+
const exitCode = await new Promise(res => proc.on('close', res));
|
|
4528
|
+
|
|
4529
|
+
if (exitCode === 0) {
|
|
4530
|
+
console.log(`\n ${color.green('✓')} Upgraded to Tova v${latestVersion}.\n`);
|
|
4531
|
+
} else {
|
|
4532
|
+
console.error(color.red(`\n Upgrade failed (exit code ${exitCode}).`));
|
|
4533
|
+
console.error(` Try manually: ${installCmd[0]} ${installCmd[1].join(' ')}\n`);
|
|
4534
|
+
process.exit(1);
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4515
4538
|
function detectPackageManager() {
|
|
4516
4539
|
if (typeof Bun !== 'undefined') return 'bun';
|
|
4517
4540
|
const ua = process.env.npm_config_user_agent || '';
|
package/package.json
CHANGED
package/src/analyzer/analyzer.js
CHANGED
|
@@ -389,6 +389,8 @@ export class Analyzer {
|
|
|
389
389
|
if (sym.extern) continue;
|
|
390
390
|
if (sym._variantOf) continue; // ADT variant constructors
|
|
391
391
|
if (name === 'main') continue;
|
|
392
|
+
// Server functions are exposed as RPC endpoints callable from client blocks
|
|
393
|
+
if (scope.context === 'server') continue;
|
|
392
394
|
|
|
393
395
|
if (!sym.used && sym.loc && sym.loc.line > 0) {
|
|
394
396
|
this.warn(`Function '${name}' is declared but never used`, sym.loc, "prefix with _ to suppress", {
|
|
@@ -2418,14 +2420,21 @@ export class Analyzer {
|
|
|
2418
2420
|
// preventing silent shadowing of immutable bindings within the same function.
|
|
2419
2421
|
_lookupAssignTarget(name) {
|
|
2420
2422
|
let scope = this.currentScope;
|
|
2423
|
+
let crossedFunction = false;
|
|
2421
2424
|
while (scope) {
|
|
2422
2425
|
const sym = scope.symbols.get(name);
|
|
2423
|
-
if (sym)
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2426
|
+
if (sym) {
|
|
2427
|
+
// After crossing a function boundary, only resolve to mutable symbols (var/state)
|
|
2428
|
+
// so that inner functions can reassign outer var/state but not shadow immutables
|
|
2429
|
+
if (crossedFunction && !sym.mutable) return null;
|
|
2430
|
+
return sym;
|
|
2431
|
+
}
|
|
2432
|
+
// Track when we cross a function boundary
|
|
2433
|
+
if (scope.context === 'function') {
|
|
2434
|
+
crossedFunction = true;
|
|
2428
2435
|
}
|
|
2436
|
+
// Stop at module level
|
|
2437
|
+
if (scope.context === 'module') break;
|
|
2429
2438
|
scope = scope.parent;
|
|
2430
2439
|
}
|
|
2431
2440
|
return null;
|
|
@@ -111,15 +111,8 @@ export function installClientAnalyzer(AnalyzerClass) {
|
|
|
111
111
|
}
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
AnalyzerClass.prototype.
|
|
115
|
-
for (const
|
|
116
|
-
if (attr.type === 'JSXSpreadAttribute') {
|
|
117
|
-
this.visitExpression(attr.expression);
|
|
118
|
-
} else {
|
|
119
|
-
this.visitExpression(attr.value);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
for (const child of node.children) {
|
|
114
|
+
AnalyzerClass.prototype._visitJSXChildren = function(children) {
|
|
115
|
+
for (const child of children) {
|
|
123
116
|
if (child.type === 'JSXElement') {
|
|
124
117
|
this.visitJSXElement(child);
|
|
125
118
|
} else if (child.type === 'JSXFragment') {
|
|
@@ -132,26 +125,30 @@ export function installClientAnalyzer(AnalyzerClass) {
|
|
|
132
125
|
this.visitJSXIf(child);
|
|
133
126
|
} else if (child.type === 'JSXMatch') {
|
|
134
127
|
this.visitJSXMatch(child);
|
|
128
|
+
} else if (child.type === 'JSXText') {
|
|
129
|
+
// JSXText wraps a TemplateLiteral/StringLiteral in its .value
|
|
130
|
+
// Visit it so identifiers in interpolated strings are marked as used
|
|
131
|
+
if (child.value) this.visitExpression(child.value);
|
|
132
|
+
} else if (child.type) {
|
|
133
|
+
// Other expression-type children
|
|
134
|
+
this.visitExpression(child);
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
-
AnalyzerClass.prototype.
|
|
140
|
-
for (const
|
|
141
|
-
if (
|
|
142
|
-
this.
|
|
143
|
-
} else
|
|
144
|
-
this.
|
|
145
|
-
} else if (child.type === 'JSXExpression') {
|
|
146
|
-
this.visitExpression(child.expression);
|
|
147
|
-
} else if (child.type === 'JSXFor') {
|
|
148
|
-
this.visitJSXFor(child);
|
|
149
|
-
} else if (child.type === 'JSXIf') {
|
|
150
|
-
this.visitJSXIf(child);
|
|
151
|
-
} else if (child.type === 'JSXMatch') {
|
|
152
|
-
this.visitJSXMatch(child);
|
|
139
|
+
AnalyzerClass.prototype.visitJSXElement = function(node) {
|
|
140
|
+
for (const attr of node.attributes) {
|
|
141
|
+
if (attr.type === 'JSXSpreadAttribute') {
|
|
142
|
+
this.visitExpression(attr.expression);
|
|
143
|
+
} else {
|
|
144
|
+
this.visitExpression(attr.value);
|
|
153
145
|
}
|
|
154
146
|
}
|
|
147
|
+
this._visitJSXChildren(node.children);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
AnalyzerClass.prototype.visitJSXFragment = function(node) {
|
|
151
|
+
this._visitJSXChildren(node.children);
|
|
155
152
|
};
|
|
156
153
|
|
|
157
154
|
AnalyzerClass.prototype.visitJSXFor = function(node) {
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-runtime.js — do not edit
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.4.1";
|