tova 0.9.8 → 0.9.11

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { resolve, basename, dirname, join, relative, sep, extname } from 'path';
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync, rmSync, chmodSync, renameSync, watch as fsWatch } from 'fs';
5
- import { spawn } from 'child_process';
5
+ import { spawn, spawnSync as _spawnSync } from 'child_process';
6
6
  import { createHash as _cryptoHash } from 'crypto';
7
7
  import { createRequire as _createRequire } from 'module';
8
8
  import { Lexer } from '../src/lexer/lexer.js';
@@ -25,6 +25,78 @@ import { addToSection, removeFromSection } from '../src/config/edit-toml.js';
25
25
  import { stringifyTOML } from '../src/config/toml.js';
26
26
 
27
27
  import { VERSION } from '../src/version.js';
28
+ import { createServer as _createHttpServer } from 'http';
29
+
30
+ const _hasBun = typeof Bun !== 'undefined';
31
+
32
+ // ─── Compat: Bun.serve() fallback to Node http.createServer ─
33
+ function _compatServe({ port, fetch: fetchHandler }) {
34
+ if (_hasBun) {
35
+ return Bun.serve({ port, fetch: fetchHandler });
36
+ }
37
+ // Node.js fallback using http.createServer
38
+ return new Promise((resolve, reject) => {
39
+ const server = _createHttpServer(async (req, res) => {
40
+ try {
41
+ const url = `http://localhost:${port}${req.url}`;
42
+ const headers = new Headers();
43
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
44
+ headers.append(req.rawHeaders[i], req.rawHeaders[i + 1]);
45
+ }
46
+ const request = new Request(url, {
47
+ method: req.method,
48
+ headers,
49
+ ...(req.method !== 'GET' && req.method !== 'HEAD' ? { body: req, duplex: 'half' } : {}),
50
+ });
51
+ const response = await fetchHandler(request);
52
+ res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
53
+ if (response.body) {
54
+ const reader = response.body.getReader();
55
+ const pump = async () => {
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+ if (done) { res.end(); return; }
59
+ res.write(value);
60
+ }
61
+ };
62
+ pump().catch(() => res.end());
63
+ } else {
64
+ const buf = Buffer.from(await response.arrayBuffer());
65
+ res.end(buf);
66
+ }
67
+ } catch (err) {
68
+ res.writeHead(500);
69
+ res.end('Internal Server Error');
70
+ }
71
+ });
72
+ server.listen(port, () => resolve(server));
73
+ server.on('error', reject);
74
+ });
75
+ }
76
+
77
+ // ─── Compat: Bun.spawnSync fallback to child_process.spawnSync ─
78
+ // Accepts Bun-style opts: { stdout: 'pipe', stderr: 'pipe', cwd, timeout }
79
+ function _compatSpawnSync(cmd, args, opts) {
80
+ if (_hasBun) return Bun.spawnSync([cmd, ...args], opts);
81
+ // Translate Bun-style stdout/stderr to Node-style stdio
82
+ const nodeOpts = { ...opts };
83
+ if (!nodeOpts.stdio) {
84
+ nodeOpts.stdio = [
85
+ 'pipe',
86
+ nodeOpts.stdout === 'pipe' ? 'pipe' : (nodeOpts.stdout || 'pipe'),
87
+ nodeOpts.stderr === 'pipe' ? 'pipe' : (nodeOpts.stderr || 'pipe'),
88
+ ];
89
+ }
90
+ delete nodeOpts.stdout;
91
+ delete nodeOpts.stderr;
92
+ const result = _spawnSync(cmd, args, nodeOpts);
93
+ return {
94
+ ...result,
95
+ exitCode: result.status,
96
+ stdout: result.stdout ? (typeof result.stdout === 'string' ? result.stdout : result.stdout.toString()) : '',
97
+ stderr: result.stderr ? (typeof result.stderr === 'string' ? result.stderr : result.stderr.toString()) : '',
98
+ };
99
+ }
28
100
 
29
101
  // ─── CLI Color Helpers ──────────────────────────────────────
30
102
  const isTTY = process.stdout?.isTTY;
@@ -1417,10 +1489,17 @@ function cleanBuild(args) {
1417
1489
 
1418
1490
  async function devServer(args) {
1419
1491
  const config = resolveConfig(process.cwd());
1420
- const explicitSrc = args.filter(a => !a.startsWith('--'))[0];
1421
- const srcDir = resolve(explicitSrc || config.project.entry || '.');
1492
+ // Parse --port value first, then filter positional args (skip flag values)
1422
1493
  const explicitPort = args.find((_, i, a) => a[i - 1] === '--port');
1423
- const basePort = parseInt(explicitPort || config.dev.port || '3000');
1494
+ const basePort = parseInt(explicitPort || config.dev?.port || '3000');
1495
+ const flagsWithValues = new Set(['--port']);
1496
+ const positional = [];
1497
+ for (let i = 0; i < args.length; i++) {
1498
+ if (args[i].startsWith('--')) { if (flagsWithValues.has(args[i])) i++; continue; }
1499
+ positional.push(args[i]);
1500
+ }
1501
+ const explicitSrc = positional[0];
1502
+ const srcDir = resolve(explicitSrc || config.project?.entry || '.');
1424
1503
  const buildStrict = args.includes('--strict');
1425
1504
  const buildStrictSecurity = args.includes('--strict-security');
1426
1505
 
@@ -1438,7 +1517,7 @@ async function devServer(args) {
1438
1517
  let actualReloadPort = reloadPort;
1439
1518
  for (let attempt = 0; attempt < 10; attempt++) {
1440
1519
  try {
1441
- reloadServer = Bun.serve({
1520
+ reloadServer = await _compatServe({
1442
1521
  port: actualReloadPort,
1443
1522
  fetch(req) {
1444
1523
  return handleReloadFetch(req);
@@ -1489,7 +1568,7 @@ async function devServer(args) {
1489
1568
  for (const [dir, files] of dirGroups) {
1490
1569
  const dirName = basename(dir) === '.' ? 'app' : basename(dir);
1491
1570
  try {
1492
- const result = mergeDirectory(dir, srcDir, { strict: buildStrict, strictSecurity: buildStrictSecurity });
1571
+ const result = mergeDirectory(dir, srcDir, { strict: buildStrict, strictSecurity: buildStrictSecurity, isDev: true });
1493
1572
  if (!result) continue;
1494
1573
 
1495
1574
  const { output, single } = result;
@@ -1584,7 +1663,7 @@ async function devServer(args) {
1584
1663
 
1585
1664
  const child = spawn('bun', ['run', sf.path], {
1586
1665
  stdio: 'inherit',
1587
- env: { ...process.env, [envKey]: String(port), PORT: String(port) },
1666
+ env: { ...process.env, [envKey]: String(port), PORT: String(port), __TOVA_HMR_STATE_PATH: join(outDir, '.hmr-state.json') },
1588
1667
  });
1589
1668
 
1590
1669
  child.on('error', (err) => {
@@ -1624,7 +1703,7 @@ async function devServer(args) {
1624
1703
  '.map': 'application/json',
1625
1704
  };
1626
1705
 
1627
- const staticServer = Bun.serve({
1706
+ const staticServer = await _compatServe({
1628
1707
  port: basePort,
1629
1708
  async fetch(req) {
1630
1709
  const url = new URL(req.url);
@@ -1734,7 +1813,7 @@ async function devServer(args) {
1734
1813
 
1735
1814
  for (const [dir, files] of rebuildDirGroups) {
1736
1815
  const dirName = basename(dir) === '.' ? 'app' : basename(dir);
1737
- const result = mergeDirectory(dir, srcDir, { strict: buildStrict, strictSecurity: buildStrictSecurity });
1816
+ const result = mergeDirectory(dir, srcDir, { strict: buildStrict, strictSecurity: buildStrictSecurity, isDev: true });
1738
1817
  if (!result) continue;
1739
1818
 
1740
1819
  const { output, single } = result;
@@ -1811,7 +1890,7 @@ async function devServer(args) {
1811
1890
  const port = basePort + rebuildPortOffset;
1812
1891
  const child = spawn('bun', ['run', serverPath], {
1813
1892
  stdio: 'inherit',
1814
- env: { ...process.env, PORT: String(port) },
1893
+ env: { ...process.env, PORT: String(port), __TOVA_HMR_STATE_PATH: join(outDir, '.hmr-state.json') },
1815
1894
  });
1816
1895
  processes.push({ child, label: 'server', port });
1817
1896
  rebuildPortOffset++;
@@ -1842,6 +1921,9 @@ async function devServer(args) {
1842
1921
  for (const p of processes) {
1843
1922
  try { p.child.kill('SIGKILL'); } catch {}
1844
1923
  }
1924
+ // Clean up HMR state file for fresh start next time
1925
+ const hmrPath = join(outDir, '.hmr-state.json');
1926
+ try { if (existsSync(hmrPath)) rmSync(hmrPath); } catch {}
1845
1927
  process.exit(0);
1846
1928
  });
1847
1929
 
@@ -3373,8 +3455,8 @@ tova add github.com/yourname/${projectName}
3373
3455
 
3374
3456
  // git init (silent, only if git is available)
3375
3457
  try {
3376
- const gitProc = Bun.spawnSync(['git', 'init'], { cwd: projectDir, stdout: 'pipe', stderr: 'pipe' });
3377
- if (gitProc.exitCode === 0) {
3458
+ const gitProc = _compatSpawnSync('git', ['init'], { cwd: projectDir, stdout: 'pipe', stderr: 'pipe' });
3459
+ if ((gitProc.exitCode ?? gitProc.status) === 0) {
3378
3460
  console.log(` ${color.green('✓')} Initialized git repository`);
3379
3461
  }
3380
3462
  } catch {}
@@ -4162,6 +4244,9 @@ function hasNpmImports(code) {
4162
4244
  }
4163
4245
 
4164
4246
  async function bundleClientCode(clientCode, srcDir) {
4247
+ if (!_hasBun) {
4248
+ throw new Error('Client bundling with npm imports requires Bun. Install from https://bun.sh and run with: bun tova build --production');
4249
+ }
4165
4250
  const tmpDir = join(srcDir, '.tova-out', '.tmp-bundle');
4166
4251
  try {
4167
4252
  mkdirSync(tmpDir, { recursive: true });
@@ -4897,7 +4982,10 @@ async function productionBuild(srcDir, outDir, isStatic = false) {
4897
4982
  const allSharedCode = sharedParts.join('\n');
4898
4983
 
4899
4984
  // Generate content hash for cache busting
4900
- const hashCode = (s) => Bun.hash(s).toString(16).slice(0, 12);
4985
+ const hashCode = (s) => {
4986
+ if (_hasBun) return Bun.hash(s).toString(16).slice(0, 12);
4987
+ return _cryptoHash('md5').update(s).digest('hex').slice(0, 12);
4988
+ };
4901
4989
 
4902
4990
  // Write server bundle
4903
4991
  if (allServerCode.trim()) {
@@ -5578,7 +5666,7 @@ function collectExports(ast, filename) {
5578
5666
  return { publicExports, allNames };
5579
5667
  }
5580
5668
 
5581
- function compileWithImports(source, filename, srcDir) {
5669
+ function compileWithImports(source, filename, srcDir, options = {}) {
5582
5670
  if (compilationCache.has(filename)) {
5583
5671
  return compilationCache.get(filename);
5584
5672
  }
@@ -5683,7 +5771,7 @@ function compileWithImports(source, filename, srcDir) {
5683
5771
  }
5684
5772
  }
5685
5773
 
5686
- const codegen = new CodeGenerator(ast, filename);
5774
+ const codegen = new CodeGenerator(ast, filename, { isDev: options.isDev });
5687
5775
  const output = codegen.generate();
5688
5776
  compilationCache.set(filename, output);
5689
5777
  return output;
@@ -5819,7 +5907,7 @@ function mergeDirectory(dir, srcDir, options = {}) {
5819
5907
  // Single file — use existing per-file compilation
5820
5908
  const file = tovaFiles[0];
5821
5909
  const source = readFileSync(file, 'utf-8');
5822
- return { output: compileWithImports(source, file, srcDir), files: [file], single: true };
5910
+ return { output: compileWithImports(source, file, srcDir, { isDev: options.isDev }), files: [file], single: true };
5823
5911
  }
5824
5912
 
5825
5913
  // Parse all files in the directory
@@ -5935,7 +6023,7 @@ function mergeDirectory(dir, srcDir, options = {}) {
5935
6023
  }
5936
6024
 
5937
6025
  // Run codegen on merged AST
5938
- const codegen = new CodeGenerator(mergedAST, dir);
6026
+ const codegen = new CodeGenerator(mergedAST, dir, { isDev: options.isDev });
5939
6027
  const output = codegen.generate();
5940
6028
 
5941
6029
  // Collect source content from all files for source maps
@@ -6024,12 +6112,13 @@ async function doctorCommand() {
6024
6112
 
6025
6113
  // 2. Bun availability
6026
6114
  try {
6027
- const bunProc = Bun.spawnSync(['bun', '--version']);
6028
- const bunVer = bunProc.stdout.toString().trim();
6029
- if (bunProc.exitCode === 0 && bunVer) {
6115
+ const bunProc = _compatSpawnSync('bun', ['--version'], { stdout: 'pipe', stderr: 'pipe' });
6116
+ const bunVer = (bunProc.stdout || '').toString().trim();
6117
+ if ((bunProc.exitCode ?? bunProc.status) === 0 && bunVer) {
6030
6118
  const major = parseInt(bunVer.split('.')[0], 10);
6031
6119
  if (major >= 1) {
6032
- pass(`Bun v${bunVer}`, Bun.spawnSync(['which', 'bun']).stdout.toString().trim());
6120
+ const whichProc = _compatSpawnSync('which', ['bun'], { stdout: 'pipe', stderr: 'pipe' });
6121
+ pass(`Bun v${bunVer}`, (whichProc.stdout || '').toString().trim());
6033
6122
  } else {
6034
6123
  warn(`Bun v${bunVer}`, 'Bun >= 1.0 recommended');
6035
6124
  }
@@ -6077,9 +6166,9 @@ async function doctorCommand() {
6077
6166
 
6078
6167
  // 5. git
6079
6168
  try {
6080
- const gitProc = Bun.spawnSync(['git', '--version']);
6081
- if (gitProc.exitCode === 0) {
6082
- const gitVer = gitProc.stdout.toString().trim();
6169
+ const gitProc = _compatSpawnSync('git', ['--version'], { stdout: 'pipe', stderr: 'pipe' });
6170
+ if ((gitProc.exitCode ?? gitProc.status) === 0) {
6171
+ const gitVer = (gitProc.stdout || '').toString().trim();
6083
6172
  pass('git available', gitVer);
6084
6173
  } else {
6085
6174
  warn('git', 'not found');
@@ -6323,6 +6412,10 @@ function detectInstallMethod() {
6323
6412
  const execPath = process.execPath || process.argv[0];
6324
6413
  const scriptPath = process.argv[1] || '';
6325
6414
  if (execPath.includes('.tova/bin') || scriptPath.includes('.tova/')) return 'binary';
6415
+ // Check if ~/.tova/bin/tova exists — indicates binary/wrapper install even if
6416
+ // the wrapper points to a local repo checkout
6417
+ const wrapperPath = join(process.env.HOME || '', '.tova', 'bin', 'tova');
6418
+ if (existsSync(wrapperPath)) return 'binary';
6326
6419
  return 'npm';
6327
6420
  }
6328
6421
 
@@ -6584,8 +6677,8 @@ async function infoCommand() {
6584
6677
  // Bun version
6585
6678
  let bunVersion = 'not found';
6586
6679
  try {
6587
- const proc = Bun.spawnSync(['bun', '--version']);
6588
- bunVersion = proc.stdout.toString().trim();
6680
+ const proc = _compatSpawnSync('bun', ['--version'], { stdout: 'pipe', stderr: 'pipe' });
6681
+ bunVersion = (proc.stdout || '').toString().trim();
6589
6682
  } catch {}
6590
6683
  console.log(` Runtime: Bun v${bunVersion}`);
6591
6684
  console.log(` Platform: ${process.platform} ${process.arch}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.9.8",
3
+ "version": "0.9.11",
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",
@@ -59,6 +59,7 @@ export class CodeGenerator {
59
59
  this.ast = ast;
60
60
  this.filename = filename;
61
61
  this._sourceMaps = options.sourceMaps !== false; // default true; pass false for REPL/check
62
+ this._isDev = options.isDev || false;
62
63
  }
63
64
 
64
65
  // Group blocks by name (null name = "default")
@@ -250,6 +251,7 @@ export class CodeGenerator {
250
251
  for (const [name, blocks] of serverGroups) {
251
252
  const gen = new (getServerCodegen())();
252
253
  gen._sourceMapsEnabled = this._sourceMaps;
254
+ gen._isDev = this._isDev;
253
255
  const key = name || 'default';
254
256
  // Build peer blocks map (all named blocks except self)
255
257
  let peerBlocks = null;
@@ -240,6 +240,7 @@ export class ServerCodegen extends BaseCodegen {
240
240
  const functions = [];
241
241
  const middlewares = [];
242
242
  const otherStatements = [];
243
+ const hmrVarNames = [];
243
244
  let healthPath = null;
244
245
  let healthChecks = null;
245
246
  let corsConfig = null;
@@ -2142,8 +2143,29 @@ export class ServerCodegen extends BaseCodegen {
2142
2143
  // ════════════════════════════════════════════════════════════
2143
2144
  // 13. Other statements + Server Functions
2144
2145
  // ════════════════════════════════════════════════════════════
2146
+ if (this._isDev) {
2147
+ lines.push('// ── HMR State ──');
2148
+ lines.push('const __hmrStatePath = process.env.__TOVA_HMR_STATE_PATH || "";');
2149
+ lines.push('let __hmrState = {};');
2150
+ lines.push('if (__hmrStatePath) { try { __hmrState = JSON.parse(require("fs").readFileSync(__hmrStatePath, "utf-8")); } catch {} }');
2151
+ lines.push('');
2152
+ }
2145
2153
  for (const stmt of otherStatements) {
2146
- lines.push(this.generateStatement(stmt));
2154
+ // HMR wrapping: new variable declarations get restored from __hmrState
2155
+ const isHmrEligible = this._isDev && stmt.targets && stmt.targets.length === 1 &&
2156
+ stmt.values && stmt.values.length === 1 && typeof stmt.targets[0] === 'string' &&
2157
+ (stmt.type === 'Assignment' ? !this.isDeclared(stmt.targets[0]) : stmt.type === 'VarDeclaration');
2158
+ if (isHmrEligible) {
2159
+ const name = stmt.targets[0];
2160
+ hmrVarNames.push(name);
2161
+ this.declareVar(name);
2162
+ if (stmt.isPublic) this._userDefinedNames.add(name);
2163
+ const initExpr = this.genExpression(stmt.values[0]);
2164
+ const keyword = stmt.type === 'VarDeclaration' ? 'let' : 'const';
2165
+ lines.push(`${keyword} ${name} = (${JSON.stringify(name)} in __hmrState) ? __hmrState[${JSON.stringify(name)}] : ${initExpr};`);
2166
+ } else {
2167
+ lines.push(this.generateStatement(stmt));
2168
+ }
2147
2169
  }
2148
2170
 
2149
2171
  if (functions.length > 0) {
@@ -3688,10 +3710,24 @@ export class ServerCodegen extends BaseCodegen {
3688
3710
  // 24. Graceful Shutdown — on_stop hooks (F3) + clearInterval (F8)
3689
3711
  // ════════════════════════════════════════════════════════════
3690
3712
  lines.push('// ── Graceful Shutdown ──');
3713
+ // HMR state-save helper (emitted as first action in __shutdown)
3714
+ const hmrSaveLines = [];
3715
+ if (this._isDev && hmrVarNames.length > 0) {
3716
+ const entries = hmrVarNames.map(n => `${JSON.stringify(n)}: ${n}`).join(', ');
3717
+ hmrSaveLines.push(` try { require("fs").writeFileSync(__hmrStatePath, JSON.stringify({ ${entries} })); } catch {}`);
3718
+ }
3691
3719
  if (isFastMode) {
3692
- lines.push('function __shutdown() { __server.stop(); process.exit(0); }');
3720
+ if (hmrSaveLines.length > 0) {
3721
+ lines.push('function __shutdown() {');
3722
+ for (const l of hmrSaveLines) lines.push(l);
3723
+ lines.push(' __server.stop(); process.exit(0);');
3724
+ lines.push('}');
3725
+ } else {
3726
+ lines.push('function __shutdown() { __server.stop(); process.exit(0); }');
3727
+ }
3693
3728
  } else {
3694
3729
  lines.push('async function __shutdown() {');
3730
+ for (const l of hmrSaveLines) lines.push(l);
3695
3731
  lines.push(` console.log(\`Tova server${label} shutting down...\`);`);
3696
3732
  lines.push(' __shuttingDown = true;');
3697
3733
  lines.push(' __server.stop();');
@@ -5,7 +5,7 @@ import { TokenType } from '../lexer/tokens.js';
5
5
  import * as AST from './ast.js';
6
6
  import { AuthConfigField, AuthProviderDeclaration, AuthHookDeclaration, AuthProtectedRoute } from './auth-ast.js';
7
7
 
8
- const CONFIG_KEY_TOKENS = new Set([
8
+ const AUTH_CONFIG_KEY_TOKENS = new Set([
9
9
  TokenType.IDENTIFIER, TokenType.TYPE, TokenType.STORE,
10
10
  TokenType.FN, TokenType.MATCH, TokenType.IF,
11
11
  ]);
@@ -15,7 +15,7 @@ export function installAuthParser(ParserClass) {
15
15
  ParserClass.prototype._authParserInstalled = true;
16
16
 
17
17
  ParserClass.prototype._expectAuthConfigKey = function(context) {
18
- if (CONFIG_KEY_TOKENS.has(this.current().type)) {
18
+ if (AUTH_CONFIG_KEY_TOKENS.has(this.current().type)) {
19
19
  return this.advance().value;
20
20
  }
21
21
  this.error(`Expected ${context} config key`);
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.9.8";
2
+ export const VERSION = "0.9.11";