tova 0.11.20 → 0.11.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tova",
3
- "version": "0.11.20",
3
+ "version": "0.11.22",
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",
@@ -168,9 +168,11 @@ export class Analyzer {
168
168
  // Async
169
169
  'sleep',
170
170
  // String functions
171
- 'upper', 'lower', 'contains', 'starts_with', 'ends_with',
172
- 'chars', 'words', 'lines', 'capitalize', 'title_case',
173
- 'snake_case', 'camel_case',
171
+ 'upper', 'lower', 'contains', 'startsWith', 'endsWith',
172
+ 'starts_with', 'ends_with',
173
+ 'chars', 'words', 'lines', 'capitalize', 'titleCase',
174
+ 'snakeCase', 'camelCase',
175
+ 'title_case', 'snake_case', 'camel_case',
174
176
  // Math extras
175
177
  'min', 'max',
176
178
  // Table operations
package/src/cli/run.js CHANGED
@@ -1,11 +1,54 @@
1
1
  import { resolve, dirname, join } from 'path';
2
2
  import { readFileSync, existsSync } from 'fs';
3
3
  import { createRequire as _createRequire } from 'module';
4
+ import { pathToFileURL } from 'url';
4
5
  import { compileTova } from './compile.js';
5
6
  import { getRunStdlib } from './utils.js';
6
7
  import { resolveConfig } from '../config/resolve.js';
7
8
  import { richError } from '../diagnostics/formatter.js';
8
9
 
10
+ function findLocalTovaImports(source, fromFile) {
11
+ const importDetectRegex = /import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"]([^'"]+)['"]/gm;
12
+ let importMatch;
13
+ const tovaImportPaths = [];
14
+ while ((importMatch = importDetectRegex.exec(source)) !== null) {
15
+ const importSource = importMatch[1];
16
+ if (!importSource.startsWith('.') && !importSource.startsWith('/')) continue;
17
+ let depPath = resolve(dirname(fromFile), importSource);
18
+ if (!depPath.endsWith('.tova') && existsSync(depPath + '.tova')) {
19
+ depPath = depPath + '.tova';
20
+ }
21
+ if (depPath.endsWith('.tova') && existsSync(depPath)) {
22
+ tovaImportPaths.push({ source: importSource, resolved: depPath });
23
+ }
24
+ }
25
+ return tovaImportPaths;
26
+ }
27
+
28
+ function stripInlinedImports(code, importSources) {
29
+ let stripped = code;
30
+ for (const importSource of importSources) {
31
+ const escaped = importSource.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ stripped = stripped.replace(new RegExp('^import\\s+(?:\\{[^}]*\\}|[\\w$]+|\\*\\s+as\\s+[\\w$]+)\\s+from\\s+[\'\"]' + escaped + '[\'\"];?\\s*$', 'gm'), '');
33
+ }
34
+ return stripped;
35
+ }
36
+
37
+ function rewriteImportsForRuntime(code, baseFileUrl) {
38
+ return code.replace(/^import\s+(\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"]([^'"]+)['"];?\s*$/gm, (match, clause, source) => {
39
+ const importExpr = source.startsWith('.') || source.startsWith('/')
40
+ ? `await import(new URL(${JSON.stringify(source)}, ${JSON.stringify(baseFileUrl)}).href)`
41
+ : `await import(${JSON.stringify(source)})`;
42
+ if (clause.startsWith('{')) {
43
+ return `const ${clause.replace(/\bas\b/g, ':')} = ${importExpr};`;
44
+ }
45
+ if (clause.startsWith('*')) {
46
+ return `const ${clause.replace(/^\*\s+as\s+/, '').trim()} = ${importExpr};`;
47
+ }
48
+ return `const { default: ${clause.trim()} } = ${importExpr};`;
49
+ });
50
+ }
51
+
9
52
  export async function runFile(filePath, options = {}) {
10
53
  if (!filePath) {
11
54
  // If tova.toml exists, try to find a main file in the entry directory
@@ -34,26 +77,13 @@ export async function runFile(filePath, options = {}) {
34
77
  }
35
78
 
36
79
  const source = readFileSync(resolved, 'utf-8');
80
+ const fileUrl = pathToFileURL(resolved).href;
37
81
 
38
82
  try {
39
- // Detect local .tova imports (with or without .tova extension)
40
- const importDetectRegex = /import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"]([^'"]+)['"]/gm;
41
- let importMatch;
42
- const tovaImportPaths = [];
43
- while ((importMatch = importDetectRegex.exec(source)) !== null) {
44
- const importSource = importMatch[1];
45
- if (!importSource.startsWith('.') && !importSource.startsWith('/')) continue;
46
- let depPath = resolve(dirname(resolved), importSource);
47
- if (!depPath.endsWith('.tova') && existsSync(depPath + '.tova')) {
48
- depPath = depPath + '.tova';
49
- }
50
- if (depPath.endsWith('.tova') && existsSync(depPath)) {
51
- tovaImportPaths.push({ source: importSource, resolved: depPath });
52
- }
53
- }
83
+ const tovaImportPaths = findLocalTovaImports(source, resolved);
54
84
  const hasTovaImports = tovaImportPaths.length > 0;
55
85
 
56
- const output = compileTova(source, filePath, { strict: options.strict, strictSecurity: options.strictSecurity });
86
+ const output = compileTova(source, resolved, { strict: options.strict, strictSecurity: options.strictSecurity });
57
87
 
58
88
  // Execute the generated JavaScript (with stdlib)
59
89
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
@@ -81,26 +111,18 @@ export async function runFile(filePath, options = {}) {
81
111
  compiled.add(filePath);
82
112
 
83
113
  const depSource = readFileSync(filePath, 'utf-8');
114
+ const depImportPaths = findLocalTovaImports(depSource, filePath);
84
115
 
85
- // Scan for transitive .tova imports
86
- const transitiveRegex = /import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"]([^'"]+)['"]/g;
87
- let transitiveMatch;
88
- while ((transitiveMatch = transitiveRegex.exec(depSource)) !== null) {
89
- const importSource = transitiveMatch[1];
90
- if (!importSource.startsWith('.') && !importSource.startsWith('/')) continue;
91
- let transitivePath = resolve(dirname(filePath), importSource);
92
- if (!transitivePath.endsWith('.tova') && existsSync(transitivePath + '.tova')) {
93
- transitivePath = transitivePath + '.tova';
94
- }
95
- if (transitivePath.endsWith('.tova') && existsSync(transitivePath)) {
96
- resolveTovaImportsRecursive(transitivePath);
97
- }
116
+ for (const depImport of depImportPaths) {
117
+ resolveTovaImportsRecursive(depImport.resolved);
98
118
  }
99
119
 
100
120
  // Compile this dependency
101
121
  const dep = compileTova(depSource, filePath, { strict: options.strict });
102
122
  let depShared = dep.shared || '';
103
123
  depShared = depShared.replace(/^export /gm, '');
124
+ depShared = stripInlinedImports(depShared, depImportPaths.map(imp => imp.source));
125
+ depShared = rewriteImportsForRuntime(depShared, pathToFileURL(filePath).href);
104
126
  depCode += depShared + '\n';
105
127
  };
106
128
 
@@ -112,14 +134,8 @@ export async function runFile(filePath, options = {}) {
112
134
  let code = stdlib + '\n' + depCode + (output.shared || '') + '\n' + (output.server || output.browser || '');
113
135
  // Strip 'export ' keywords — not valid inside AsyncFunction (used in tova build only)
114
136
  code = code.replace(/^export /gm, '');
115
- // Strip import lines for local modules (already inlined above)
116
- code = code.replace(/^import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"][^'"]*\.(?:tova|(?:shared\.)?js)['"];?\s*$/gm, '');
117
- if (hasTovaImports) {
118
- for (const imp of tovaImportPaths) {
119
- const escaped = imp.source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
120
- code = code.replace(new RegExp('^import\\s+(?:\\{[^}]*\\}|[\\w$]+|\\*\\s+as\\s+[\\w$]+)\\s+from\\s+[\'"]' + escaped + '[\'"];?\\s*$', 'gm'), '');
121
- }
122
- }
137
+ code = stripInlinedImports(code, tovaImportPaths.map(imp => imp.source));
138
+ code = rewriteImportsForRuntime(code, fileUrl);
123
139
  // Auto-call main() if the compiled code defines a main function
124
140
  const scriptArgs = options.scriptArgs || [];
125
141
  if (/\bfunction\s+main\s*\(/.test(code)) {
package/src/lsp/server.js CHANGED
@@ -872,18 +872,26 @@ class TovaLanguageServer {
872
872
  'upper': '`fn upper(s) -> String` — Convert to uppercase',
873
873
  'lower': '`fn lower(s) -> String` — Convert to lowercase',
874
874
  'contains': '`fn contains(s, sub) -> Bool` — Check if string contains substring',
875
- 'starts_with': '`fn starts_with(s, prefix) -> Bool` — Check if string starts with prefix',
876
- 'ends_with': '`fn ends_with(s, suffix) -> Bool` — Check if string ends with suffix',
875
+ 'startsWith': '`fn startsWith(s, prefix) -> Bool` — Check if string starts with prefix',
876
+ 'starts_with': '`fn startsWith(s, prefix) -> Bool` — Check if string starts with prefix',
877
+ 'endsWith': '`fn endsWith(s, suffix) -> Bool` — Check if string ends with suffix',
878
+ 'ends_with': '`fn endsWith(s, suffix) -> Bool` — Check if string ends with suffix',
877
879
  'chars': '`fn chars(s) -> [String]` — Split string into individual characters',
878
880
  'words': '`fn words(s) -> [String]` — Split by whitespace',
879
881
  'lines': '`fn lines(s) -> [String]` — Split by newlines',
880
882
  'capitalize': '`fn capitalize(s) -> String` — Capitalize first letter',
881
- 'title_case': '`fn title_case(s) -> String` — Capitalize each word',
882
- 'snake_case': '`fn snake_case(s) -> String` — Convert to snake_case',
883
- 'camel_case': '`fn camel_case(s) -> String` — Convert to camelCase',
884
- 'kebab_case': '`fn kebab_case(s) -> String` — Convert to kebab-case',
885
- 'pad_start': '`fn pad_start(s, n, fill?) -> String` — Pad start to length n',
886
- 'pad_end': '`fn pad_end(s, n, fill?) -> String` — Pad end to length n',
883
+ 'titleCase': '`fn titleCase(s) -> String` — Capitalize each word',
884
+ 'title_case': '`fn titleCase(s) -> String` — Capitalize each word',
885
+ 'snakeCase': '`fn snakeCase(s) -> String` — Convert to snake_case',
886
+ 'snake_case': '`fn snakeCase(s) -> String` — Convert to snake_case',
887
+ 'camelCase': '`fn camelCase(s) -> String` — Convert to camelCase',
888
+ 'camel_case': '`fn camelCase(s) -> String` — Convert to camelCase',
889
+ 'kebabCase': '`fn kebabCase(s) -> String` — Convert to kebab-case',
890
+ 'kebab_case': '`fn kebabCase(s) -> String` — Convert to kebab-case',
891
+ 'padStart': '`fn padStart(s, n, fill?) -> String` — Pad start to length n',
892
+ 'pad_start': '`fn padStart(s, n, fill?) -> String` — Pad start to length n',
893
+ 'padEnd': '`fn padEnd(s, n, fill?) -> String` — Pad end to length n',
894
+ 'pad_end': '`fn padEnd(s, n, fill?) -> String` — Pad end to length n',
887
895
  'char_at': '`fn char_at(s, i) -> String?` — Character at index (nil if out of bounds)',
888
896
  'index_of': '`fn index_of(s, sub) -> Int?` — Index of first occurrence (nil if not found)',
889
897
  'last_index_of': '`fn last_index_of(s, sub) -> Int?` — Index of last occurrence',
@@ -11,8 +11,11 @@ const methods = {
11
11
  words() { return this.split(/\s+/).filter(Boolean); },
12
12
  lines() { return this.split('\n'); },
13
13
  capitalize() { return this.length ? this.charAt(0).toUpperCase() + this.slice(1) : this; },
14
+ titleCase() { return this.replace(/\b\w/g, c => c.toUpperCase()); },
14
15
  title_case() { return this.replace(/\b\w/g, c => c.toUpperCase()); },
16
+ snakeCase() { return this.replace(/[-\s]+/g, '_').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase().replace(/^_/, ''); },
15
17
  snake_case() { return this.replace(/[-\s]+/g, '_').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase().replace(/^_/, ''); },
18
+ camelCase() { return this.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '').replace(/^[A-Z]/, c => c.toLowerCase()); },
16
19
  camel_case() { return this.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '').replace(/^[A-Z]/, c => c.toLowerCase()); },
17
20
  };
18
21
 
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.11.20";
2
+ export const VERSION = "0.11.22";