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.
- package/bin/tova.js +261 -60
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +351 -11
- package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/form-analyzer.js +113 -0
- package/src/analyzer/scope.js +2 -2
- package/src/codegen/base-codegen.js +1160 -10
- package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
- package/src/codegen/codegen.js +119 -28
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/edge-codegen.js +1351 -0
- package/src/codegen/form-codegen.js +553 -0
- package/src/codegen/security-codegen.js +5 -5
- package/src/codegen/server-codegen.js +88 -7
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/wasm-codegen.js +6 -0
- package/src/config/edit-toml.js +6 -2
- package/src/config/git-resolver.js +128 -0
- package/src/config/lock-file.js +57 -0
- package/src/config/module-cache.js +58 -0
- package/src/config/module-entry.js +37 -0
- package/src/config/module-path.js +31 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +17 -0
- package/src/config/resolver.js +139 -0
- package/src/config/search.js +28 -0
- package/src/config/semver.js +72 -0
- package/src/config/toml.js +48 -5
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +311 -0
- package/src/diagnostics/error-codes.js +1 -1
- package/src/docs/generator.js +1 -1
- package/src/formatter/formatter.js +4 -4
- package/src/lexer/tokens.js +12 -2
- package/src/lsp/server.js +483 -1
- package/src/parser/ast.js +60 -5
- package/src/parser/{client-ast.js → browser-ast.js} +3 -3
- package/src/parser/{client-parser.js → browser-parser.js} +42 -15
- package/src/parser/concurrency-ast.js +15 -0
- package/src/parser/concurrency-parser.js +236 -0
- package/src/parser/deploy-ast.js +37 -0
- package/src/parser/deploy-parser.js +132 -0
- package/src/parser/edge-ast.js +83 -0
- package/src/parser/edge-parser.js +262 -0
- package/src/parser/form-ast.js +80 -0
- package/src/parser/form-parser.js +206 -0
- package/src/parser/parser.js +82 -14
- package/src/parser/select-ast.js +39 -0
- package/src/registry/plugins/browser-plugin.js +30 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/edge-plugin.js +32 -0
- package/src/registry/register-all.js +8 -2
- package/src/runtime/ssr.js +2 -2
- package/src/stdlib/inline.js +38 -6
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/version.js +1 -1
- 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
|
-
|
|
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.
|
|
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
|
|
837
|
-
if (output.
|
|
838
|
-
const
|
|
839
|
-
writeFileSync(
|
|
840
|
-
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.',
|
|
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
|
|
854
|
-
if (output.multiBlock && output.
|
|
855
|
-
for (const [name, code] of Object.entries(output.
|
|
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}.
|
|
963
|
+
const path = join(outDir, `${outBaseName}.browser.${name}.js`);
|
|
858
964
|
writeFileSync(path, code);
|
|
859
|
-
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', path)} [
|
|
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.
|
|
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.
|
|
1120
|
-
const p = join(outDir, `${outBaseName}.
|
|
1121
|
-
writeFileSync(p, output.
|
|
1122
|
-
clientHTML = await generateDevHTML(output.
|
|
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.
|
|
1154
|
-
for (const [name, code] of Object.entries(output.
|
|
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}.
|
|
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.
|
|
1268
|
-
writeFileSync(join(outDir, `${outBaseName}.
|
|
1269
|
-
rebuildClientHTML = await generateDevHTML(output.
|
|
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
|
-
|
|
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
|
-
|
|
1972
|
-
|
|
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 (
|
|
2174
|
+
if (pkgName.startsWith('file:') || pkgName.startsWith('./') || pkgName.startsWith('../') || pkgName.startsWith('/')) {
|
|
1976
2175
|
// Local path dependency
|
|
1977
|
-
source =
|
|
1978
|
-
name = basename(
|
|
1979
|
-
} else if (
|
|
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 =
|
|
1982
|
-
name = basename(
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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 =
|
|
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 === '
|
|
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 === '
|
|
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
|
|
3857
|
-
const
|
|
3858
|
-
for (const block of mergedBlocks.
|
|
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 (
|
|
3863
|
-
else
|
|
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 (
|
|
3868
|
-
else
|
|
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 (
|
|
3874
|
-
else
|
|
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 (
|
|
3878
|
-
else
|
|
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 (
|
|
3881
|
-
else
|
|
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
|
|
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 === '
|
|
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,
|
|
4254
|
+
validateMergedAST({ sharedBlocks, serverBlocks, browserBlocks }, tovaFiles);
|
|
4054
4255
|
|
|
4055
4256
|
// Build merged Program AST
|
|
4056
4257
|
const mergedAST = new Program(mergedBody);
|