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.
- package/bin/tova.js +192 -10
- package/package.json +2 -7
- package/src/analyzer/analyzer.js +134 -2
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/codegen/base-codegen.js +1159 -10
- package/src/codegen/codegen.js +20 -0
- package/src/codegen/deploy-codegen.js +49 -0
- 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/lsp/server.js +482 -0
- package/src/parser/ast.js +24 -0
- 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/parser.js +21 -3
- package/src/parser/select-ast.js +39 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/register-all.js +4 -0
- package/src/stdlib/inline.js +35 -3
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/version.js +1 -1
package/src/codegen/codegen.js
CHANGED
|
@@ -36,6 +36,12 @@ function getEdgeCodegen() {
|
|
|
36
36
|
return _EdgeCodegen;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
let _DeployCodegen;
|
|
40
|
+
function getDeployCodegen() {
|
|
41
|
+
if (!_DeployCodegen) _DeployCodegen = _require('./deploy-codegen.js').DeployCodegen;
|
|
42
|
+
return _DeployCodegen;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
export class CodeGenerator {
|
|
40
46
|
constructor(ast, filename = '<stdin>', options = {}) {
|
|
41
47
|
this.ast = ast;
|
|
@@ -87,6 +93,7 @@ export class CodeGenerator {
|
|
|
87
93
|
const dataBlocks = getBlocks('data');
|
|
88
94
|
const securityBlocks = getBlocks('security');
|
|
89
95
|
const edgeBlocks = getBlocks('edge');
|
|
96
|
+
const deployBlocks = getBlocks('deploy');
|
|
90
97
|
|
|
91
98
|
// Detect module mode: no blocks, only top-level statements
|
|
92
99
|
const hasAnyBlocks = BlockRegistry.all().some(p => getBlocks(p.name).length > 0);
|
|
@@ -232,6 +239,17 @@ export class CodeGenerator {
|
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
|
|
242
|
+
// Generate deploy configs (one per named block)
|
|
243
|
+
const deploys = {};
|
|
244
|
+
if (deployBlocks.length > 0) {
|
|
245
|
+
const Deploy = getDeployCodegen();
|
|
246
|
+
const deployGroups = this._groupByName(deployBlocks);
|
|
247
|
+
for (const [name, blocks] of deployGroups) {
|
|
248
|
+
const key = name || 'default';
|
|
249
|
+
deploys[key] = Deploy.mergeDeployBlocks(blocks);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
235
253
|
// Generate tests if test blocks exist
|
|
236
254
|
let testCode = '';
|
|
237
255
|
if (testBlocks.length > 0) {
|
|
@@ -267,6 +285,7 @@ export class CodeGenerator {
|
|
|
267
285
|
browser: browserCode,
|
|
268
286
|
client: browserCode, // deprecated alias for backward compat
|
|
269
287
|
edge: edges['default'] || '',
|
|
288
|
+
deploy: Object.keys(deploys).length > 0 ? deploys : undefined,
|
|
270
289
|
sourceMappings,
|
|
271
290
|
_sourceFile: this.filename,
|
|
272
291
|
};
|
|
@@ -287,6 +306,7 @@ export class CodeGenerator {
|
|
|
287
306
|
browsers, // { "admin": code, "dashboard": code, ... }
|
|
288
307
|
clients: browsers, // deprecated alias for backward compat
|
|
289
308
|
edges, // { "api": code, "assets": code, ... }
|
|
309
|
+
deploy: Object.keys(deploys).length > 0 ? deploys : undefined,
|
|
290
310
|
multiBlock: true,
|
|
291
311
|
sourceMappings,
|
|
292
312
|
_sourceFile: this.filename,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Deploy-specific codegen for the Tova language
|
|
2
|
+
// Produces a configuration manifest (plain JS object) from deploy block AST nodes.
|
|
3
|
+
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
instances: 1,
|
|
6
|
+
memory: '512mb',
|
|
7
|
+
branch: 'main',
|
|
8
|
+
health: '/healthz',
|
|
9
|
+
health_interval: 30,
|
|
10
|
+
health_timeout: 5,
|
|
11
|
+
restart_on_failure: true,
|
|
12
|
+
keep_releases: 5,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class DeployCodegen {
|
|
16
|
+
static mergeDeployBlocks(blocks) {
|
|
17
|
+
const config = { ...DEFAULTS, env: {}, databases: [] };
|
|
18
|
+
for (const block of blocks) {
|
|
19
|
+
config.name = block.name;
|
|
20
|
+
for (const stmt of block.body) {
|
|
21
|
+
switch (stmt.type) {
|
|
22
|
+
case 'DeployConfigField': {
|
|
23
|
+
// Extract literal value from AST expression
|
|
24
|
+
const val = stmt.value;
|
|
25
|
+
config[stmt.key] = val.value !== undefined ? val.value : val;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
case 'DeployEnvBlock': {
|
|
29
|
+
for (const entry of stmt.entries) {
|
|
30
|
+
config.env[entry.key] = entry.value.value !== undefined ? entry.value.value : entry.value;
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case 'DeployDbBlock': {
|
|
35
|
+
const dbConfig = {};
|
|
36
|
+
if (stmt.config && typeof stmt.config === 'object') {
|
|
37
|
+
for (const [k, v] of Object.entries(stmt.config)) {
|
|
38
|
+
dbConfig[k] = v.value !== undefined ? v.value : v;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
config.databases.push({ engine: stmt.engine, config: dbConfig });
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return config;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -10,6 +10,11 @@ export class SharedCodegen extends BaseCodegen {
|
|
|
10
10
|
// Generate any needed helpers (called after all code is generated)
|
|
11
11
|
generateHelpers() {
|
|
12
12
|
const helpers = [];
|
|
13
|
+
// Runtime bridge for WASM-Tokio concurrent execution
|
|
14
|
+
if (this._needsRuntimeBridge) {
|
|
15
|
+
// Try multiple paths: relative to script, package require, absolute from process.cwd()
|
|
16
|
+
helpers.push(`let __tova_rt = null; try { const __p = require('path'); const __d = __p.dirname(typeof __filename !== 'undefined' ? __filename : process.argv[1] || ''); const __candidates = [__p.join(__d, '..', 'src', 'stdlib', 'runtime-bridge.js'), __p.join(process.cwd(), 'src', 'stdlib', 'runtime-bridge.js')]; for (const __c of __candidates) { try { __tova_rt = require(__c); break; } catch(_) {} } } catch(_) {}`);
|
|
17
|
+
}
|
|
13
18
|
helpers.push(this.getStringProtoHelper());
|
|
14
19
|
// Only include Result/Option if Ok/Err/Some/None are used
|
|
15
20
|
if (this._needsResultOption) {
|
|
@@ -602,6 +602,12 @@ export function generateWasmGlue(funcNode, wasmBytes) {
|
|
|
602
602
|
return `const ${name} = new WebAssembly.Instance(new WebAssembly.Module(new Uint8Array([${bytesStr}]))).exports.${name};`;
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
+
// Generate a module-level constant holding the raw WASM bytes for runtime use
|
|
606
|
+
export function generateWasmBytesExport(funcName, wasmBytes) {
|
|
607
|
+
const bytesStr = Array.from(wasmBytes).join(',');
|
|
608
|
+
return `const __wasm_bytes_${funcName} = new Uint8Array([${bytesStr}]);`;
|
|
609
|
+
}
|
|
610
|
+
|
|
605
611
|
// Generate JS glue code for a multi-function WASM module
|
|
606
612
|
export function generateMultiWasmGlue(funcNodes, wasmBytes) {
|
|
607
613
|
const bytesStr = Array.from(wasmBytes).join(',');
|
package/src/config/edit-toml.js
CHANGED
|
@@ -28,13 +28,15 @@ export function addToSection(filePath, section, key, value) {
|
|
|
28
28
|
const endIdx = findSectionEnd(lines, sectionIdx);
|
|
29
29
|
|
|
30
30
|
// Check if key already exists in this section — update it
|
|
31
|
+
const bareKey = key.replace(/^"|"$/g, '');
|
|
31
32
|
for (let i = sectionIdx + 1; i < endIdx; i++) {
|
|
32
33
|
const line = lines[i].trim();
|
|
33
34
|
if (line === '' || line.startsWith('#')) continue;
|
|
34
35
|
const eqIdx = line.indexOf('=');
|
|
35
36
|
if (eqIdx !== -1) {
|
|
36
37
|
const existingKey = line.slice(0, eqIdx).trim();
|
|
37
|
-
|
|
38
|
+
const existingBare = existingKey.replace(/^"|"$/g, '');
|
|
39
|
+
if (existingKey === key || existingBare === bareKey) {
|
|
38
40
|
lines[i] = entry;
|
|
39
41
|
writeFileSync(filePath, lines.join('\n'));
|
|
40
42
|
return;
|
|
@@ -57,6 +59,7 @@ export function addToSection(filePath, section, key, value) {
|
|
|
57
59
|
export function removeFromSection(filePath, section, key) {
|
|
58
60
|
const content = readFileSync(filePath, 'utf-8');
|
|
59
61
|
const lines = content.split('\n');
|
|
62
|
+
const bareKey = key.replace(/^"|"$/g, '');
|
|
60
63
|
|
|
61
64
|
const sectionIdx = findSectionIndex(lines, section);
|
|
62
65
|
if (sectionIdx === -1) return false;
|
|
@@ -69,7 +72,8 @@ export function removeFromSection(filePath, section, key) {
|
|
|
69
72
|
const eqIdx = line.indexOf('=');
|
|
70
73
|
if (eqIdx !== -1) {
|
|
71
74
|
const existingKey = line.slice(0, eqIdx).trim();
|
|
72
|
-
|
|
75
|
+
const existingBare = existingKey.replace(/^"|"$/g, '');
|
|
76
|
+
if (existingKey === key || existingBare === bareKey) {
|
|
73
77
|
lines.splice(i, 1);
|
|
74
78
|
writeFileSync(filePath, lines.join('\n'));
|
|
75
79
|
return true;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Git resolver for the Tova package manager.
|
|
2
|
+
// Handles git operations: parsing tag lists, sorting by semver,
|
|
3
|
+
// picking the latest tag, and fetching modules from remote repositories.
|
|
4
|
+
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { mkdirSync, renameSync, rmSync, existsSync } from 'fs';
|
|
8
|
+
import { parseSemver, compareSemver } from './semver.js';
|
|
9
|
+
import { moduleToGitUrl, parseModulePath } from './module-path.js';
|
|
10
|
+
import { getModuleCachePath, getCacheDir } from './module-cache.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse `git ls-remote --tags` output into an array of { version, sha } objects.
|
|
14
|
+
* Filters out non-semver tags and prefers dereferenced (^{}) SHAs for annotated tags.
|
|
15
|
+
*/
|
|
16
|
+
export function parseTagList(output) {
|
|
17
|
+
if (!output.trim()) return [];
|
|
18
|
+
const lines = output.trim().split('\n');
|
|
19
|
+
const tagMap = new Map();
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const [sha, ref] = line.split('\t');
|
|
22
|
+
if (!ref || !ref.startsWith('refs/tags/')) continue;
|
|
23
|
+
const tagName = ref.replace('refs/tags/', '');
|
|
24
|
+
const isDeref = tagName.endsWith('^{}');
|
|
25
|
+
const cleanName = isDeref ? tagName.slice(0, -3) : tagName;
|
|
26
|
+
const versionStr = cleanName.startsWith('v') ? cleanName.slice(1) : cleanName;
|
|
27
|
+
try { parseSemver(versionStr); } catch { continue; }
|
|
28
|
+
if (isDeref || !tagMap.has(versionStr)) {
|
|
29
|
+
tagMap.set(versionStr, sha);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return Array.from(tagMap.entries()).map(([version, sha]) => ({ version, sha }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sort an array of { version, sha } tags by semver in ascending order.
|
|
37
|
+
* Returns a new array (does not mutate the input).
|
|
38
|
+
*/
|
|
39
|
+
export function sortTags(tags) {
|
|
40
|
+
return [...tags].sort((a, b) => compareSemver(a.version, b.version));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pick the tag with the highest semver version.
|
|
45
|
+
* Returns null if the tags array is empty.
|
|
46
|
+
*/
|
|
47
|
+
export function pickLatestTag(tags) {
|
|
48
|
+
if (tags.length === 0) return null;
|
|
49
|
+
const sorted = sortTags(tags);
|
|
50
|
+
return sorted[sorted.length - 1];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* List all semver tags from a remote git repository.
|
|
55
|
+
* Returns a promise resolving to an array of { version, sha } objects.
|
|
56
|
+
*/
|
|
57
|
+
export function listRemoteTags(modulePath) {
|
|
58
|
+
const gitUrl = moduleToGitUrl(modulePath);
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const proc = spawn('git', ['ls-remote', '--tags', gitUrl], {
|
|
61
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
62
|
+
});
|
|
63
|
+
let stdout = '';
|
|
64
|
+
let stderr = '';
|
|
65
|
+
proc.stdout.on('data', d => stdout += d);
|
|
66
|
+
proc.stderr.on('data', d => stderr += d);
|
|
67
|
+
proc.on('close', code => {
|
|
68
|
+
if (code !== 0) {
|
|
69
|
+
reject(new Error(`Failed to list tags for ${modulePath}: ${stderr.trim()}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
resolve(parseTagList(stdout));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Clone a specific version of a module into the local cache.
|
|
79
|
+
* Performs a shallow clone, removes .git directory, and moves to the cache path.
|
|
80
|
+
* Returns the destination path on success.
|
|
81
|
+
*/
|
|
82
|
+
export function fetchModule(modulePath, version, cacheDir) {
|
|
83
|
+
const gitUrl = moduleToGitUrl(modulePath);
|
|
84
|
+
const destPath = getModuleCachePath(modulePath, version, cacheDir);
|
|
85
|
+
const dir = getCacheDir(cacheDir);
|
|
86
|
+
const tmpPath = join(dir, '.tmp-' + Date.now());
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const proc = spawn('git', [
|
|
89
|
+
'clone', '--depth', '1', '--branch', `v${version}`, gitUrl, tmpPath,
|
|
90
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
91
|
+
let stderr = '';
|
|
92
|
+
proc.stderr.on('data', d => stderr += d);
|
|
93
|
+
proc.on('close', code => {
|
|
94
|
+
if (code !== 0) {
|
|
95
|
+
if (existsSync(tmpPath)) rmSync(tmpPath, { recursive: true, force: true });
|
|
96
|
+
reject(new Error(`Failed to fetch ${modulePath}@v${version}: ${stderr.trim()}`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const dotGit = join(tmpPath, '.git');
|
|
100
|
+
if (existsSync(dotGit)) rmSync(dotGit, { recursive: true, force: true });
|
|
101
|
+
mkdirSync(join(destPath, '..'), { recursive: true });
|
|
102
|
+
renameSync(tmpPath, destPath);
|
|
103
|
+
resolve(destPath);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the commit SHA for a specific version tag from a remote repository.
|
|
110
|
+
* Prefers the dereferenced SHA for annotated tags.
|
|
111
|
+
* Returns null if the version tag is not found.
|
|
112
|
+
*/
|
|
113
|
+
export function getCommitSha(modulePath, version, cacheDir) {
|
|
114
|
+
const gitUrl = moduleToGitUrl(modulePath);
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const proc = spawn('git', ['ls-remote', gitUrl, `refs/tags/v${version}^{}`, `refs/tags/v${version}`], {
|
|
117
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
|
+
});
|
|
119
|
+
let stdout = '';
|
|
120
|
+
proc.stdout.on('data', d => stdout += d);
|
|
121
|
+
proc.on('close', code => {
|
|
122
|
+
if (code !== 0) { reject(new Error('Failed to get SHA')); return; }
|
|
123
|
+
const tags = parseTagList(stdout);
|
|
124
|
+
const tag = tags.find(t => t.version === version);
|
|
125
|
+
resolve(tag ? tag.sha : null);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/config/lock-file.js
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { parseTOML } from './toml.js';
|
|
5
|
+
|
|
6
|
+
export function writeLockFile(cwd, resolvedModules, npmDeps) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
lines.push('[lock]');
|
|
9
|
+
lines.push(`generated = "${new Date().toISOString()}"`);
|
|
10
|
+
lines.push('');
|
|
11
|
+
|
|
12
|
+
for (const [mod, info] of Object.entries(resolvedModules)) {
|
|
13
|
+
lines.push(`["${mod}"]`);
|
|
14
|
+
lines.push(`version = "${info.version}"`);
|
|
15
|
+
lines.push(`sha = "${info.sha}"`);
|
|
16
|
+
lines.push(`source = "${info.source}"`);
|
|
17
|
+
lines.push('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (npmDeps && Object.keys(npmDeps).length > 0) {
|
|
21
|
+
lines.push('[npm]');
|
|
22
|
+
for (const [name, version] of Object.entries(npmDeps)) {
|
|
23
|
+
lines.push(`${name} = "${version}"`);
|
|
24
|
+
}
|
|
25
|
+
lines.push('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
writeFileSync(join(cwd, 'tova.lock'), lines.join('\n'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readLockFile(cwd) {
|
|
32
|
+
const lockPath = join(cwd, 'tova.lock');
|
|
33
|
+
if (!existsSync(lockPath)) return null;
|
|
34
|
+
const content = readFileSync(lockPath, 'utf-8');
|
|
35
|
+
const parsed = parseTOML(content);
|
|
36
|
+
|
|
37
|
+
const modules = {};
|
|
38
|
+
const npm = {};
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
41
|
+
if (key === 'lock') continue;
|
|
42
|
+
if (key === 'npm') {
|
|
43
|
+
Object.assign(npm, value);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Module entries are quoted keys like "github.com/alice/http"
|
|
47
|
+
if (typeof value === 'object' && value.version) {
|
|
48
|
+
modules[key] = {
|
|
49
|
+
version: value.version,
|
|
50
|
+
sha: value.sha,
|
|
51
|
+
source: value.source,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { modules, npm, generated: parsed.lock?.generated };
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Global cache manager for the Tova package manager.
|
|
2
|
+
// Manages the ~/.tova/pkg/ directory where downloaded packages are stored.
|
|
3
|
+
// Provides path resolution, version lookup, and cleanup utilities.
|
|
4
|
+
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { existsSync, readdirSync, rmSync } from 'fs';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { compareSemver } from './semver.js';
|
|
9
|
+
import { parseModulePath } from './module-path.js';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CACHE = join(homedir(), '.tova', 'pkg');
|
|
12
|
+
|
|
13
|
+
export function getCacheDir(override) {
|
|
14
|
+
return override || process.env.TOVA_CACHE_DIR || DEFAULT_CACHE;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getModuleCachePath(modulePath, version, cacheDir) {
|
|
18
|
+
const dir = getCacheDir(cacheDir);
|
|
19
|
+
const parsed = typeof modulePath === 'string' ? parseModulePath(modulePath) : modulePath;
|
|
20
|
+
return join(dir, parsed.host, parsed.owner, parsed.repo, `v${version}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isVersionCached(modulePath, version, cacheDir) {
|
|
24
|
+
const p = getModuleCachePath(modulePath, version, cacheDir);
|
|
25
|
+
return existsSync(p) && existsSync(join(p, 'tova.toml'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function listCachedVersions(modulePath, cacheDir) {
|
|
29
|
+
const dir = getCacheDir(cacheDir);
|
|
30
|
+
const parsed = typeof modulePath === 'string' ? parseModulePath(modulePath) : modulePath;
|
|
31
|
+
const moduleDir = join(dir, parsed.host, parsed.owner, parsed.repo);
|
|
32
|
+
if (!existsSync(moduleDir)) return [];
|
|
33
|
+
const entries = readdirSync(moduleDir).filter(e => e.startsWith('v'));
|
|
34
|
+
const versions = entries.map(e => e.slice(1));
|
|
35
|
+
return versions.sort((a, b) => compareSemver(a, b));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getCompileCachePath(modulePath, version, cacheDir) {
|
|
39
|
+
const dir = getCacheDir(cacheDir);
|
|
40
|
+
return join(dir, '.cache', modulePath, `v${version}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function cleanUnusedVersions(modulePath, keepVersions, cacheDir) {
|
|
44
|
+
const dir = getCacheDir(cacheDir);
|
|
45
|
+
const parsed = typeof modulePath === 'string' ? parseModulePath(modulePath) : modulePath;
|
|
46
|
+
const moduleDir = join(dir, parsed.host, parsed.owner, parsed.repo);
|
|
47
|
+
if (!existsSync(moduleDir)) return [];
|
|
48
|
+
const entries = readdirSync(moduleDir).filter(e => e.startsWith('v'));
|
|
49
|
+
const keepSet = new Set(keepVersions.map(v => `v${v}`));
|
|
50
|
+
const removed = [];
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!keepSet.has(entry)) {
|
|
53
|
+
rmSync(join(moduleDir, entry), { recursive: true, force: true });
|
|
54
|
+
removed.push(entry.slice(1));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return removed.sort((a, b) => compareSemver(a, b));
|
|
58
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/config/module-entry.js
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
const ENTRY_CANDIDATES = [
|
|
6
|
+
'src/lib.tova',
|
|
7
|
+
'lib.tova',
|
|
8
|
+
'index.tova',
|
|
9
|
+
'src/index.tova',
|
|
10
|
+
'src/main.tova',
|
|
11
|
+
'main.tova',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Finds the entry point .tova file for a module.
|
|
16
|
+
* Checks: explicit entry → src/lib.tova → lib.tova → index.tova
|
|
17
|
+
*/
|
|
18
|
+
export function findEntryPoint(moduleDir, explicitEntry, subpath) {
|
|
19
|
+
const base = subpath ? join(moduleDir, subpath) : moduleDir;
|
|
20
|
+
|
|
21
|
+
if (explicitEntry) {
|
|
22
|
+
const p = join(base, explicitEntry);
|
|
23
|
+
if (existsSync(p)) return p;
|
|
24
|
+
throw new Error(`Explicit entry point not found: ${explicitEntry} in ${base}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const candidate of ENTRY_CANDIDATES) {
|
|
28
|
+
const p = join(base, candidate);
|
|
29
|
+
if (existsSync(p)) return p;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error(
|
|
33
|
+
`No entry point found in ${base}\n` +
|
|
34
|
+
` Looked for: ${ENTRY_CANDIDATES.join(', ')}\n` +
|
|
35
|
+
` Tip: Add an \`entry\` field to the package's tova.toml.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Module path utilities for Tova package management.
|
|
2
|
+
// Determines whether an import is a Tova module (vs npm/relative),
|
|
3
|
+
// parses module paths into components, and converts them to git URLs.
|
|
4
|
+
|
|
5
|
+
export function isTovModule(source) {
|
|
6
|
+
if (!source || source.startsWith('.') || source.startsWith('/') || source.startsWith('@') || source.includes(':')) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const firstSegment = source.split('/')[0];
|
|
10
|
+
return firstSegment.includes('.');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseModulePath(source) {
|
|
14
|
+
if (!isTovModule(source)) {
|
|
15
|
+
throw new Error(`Invalid Tova module path: "${source}"`);
|
|
16
|
+
}
|
|
17
|
+
const parts = source.split('/');
|
|
18
|
+
if (parts.length < 3) {
|
|
19
|
+
throw new Error(`Invalid Tova module path: "${source}" — expected at least host/owner/repo`);
|
|
20
|
+
}
|
|
21
|
+
const host = parts[0];
|
|
22
|
+
const owner = parts[1];
|
|
23
|
+
const repo = parts[2];
|
|
24
|
+
const subpath = parts.length > 3 ? parts.slice(3).join('/') : null;
|
|
25
|
+
return { host, owner, repo, subpath, full: `${host}/${owner}/${repo}` };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function moduleToGitUrl(modulePath) {
|
|
29
|
+
const parsed = typeof modulePath === 'string' ? parseModulePath(modulePath) : modulePath;
|
|
30
|
+
return `https://${parsed.full}.git`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/config/pkg-errors.js
|
|
2
|
+
|
|
3
|
+
export function formatVersionConflict(modulePath, sources) {
|
|
4
|
+
const lines = [`error: version conflict for ${modulePath}`, ''];
|
|
5
|
+
for (const s of sources) {
|
|
6
|
+
lines.push(` ${s.source} requires ${s.constraint}`);
|
|
7
|
+
}
|
|
8
|
+
lines.push('');
|
|
9
|
+
lines.push(' These constraints cannot be satisfied simultaneously.');
|
|
10
|
+
lines.push(' Tip: Check if either dependency has a newer version that resolves this.');
|
|
11
|
+
return lines.join('\n');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatFetchError(modulePath, detail, cachedVersions = []) {
|
|
15
|
+
const lines = [`error: failed to fetch ${modulePath}`, '', ` ${detail}`];
|
|
16
|
+
if (cachedVersions.length > 0) {
|
|
17
|
+
lines.push('');
|
|
18
|
+
lines.push(` Cached versions available: ${cachedVersions.join(', ')}`);
|
|
19
|
+
lines.push(' Tip: Run with --offline to use cached versions only.');
|
|
20
|
+
}
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatMissingEntry(modulePath, version) {
|
|
25
|
+
return [
|
|
26
|
+
`error: no entry point found for ${modulePath}@v${version}`,
|
|
27
|
+
'',
|
|
28
|
+
' Looked for: src/lib.tova, lib.tova, index.tova',
|
|
29
|
+
" Tip: The package may need an `entry` field in its tova.toml.",
|
|
30
|
+
].join('\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatAuthError(modulePath) {
|
|
34
|
+
return [
|
|
35
|
+
`error: authentication failed for ${modulePath}`,
|
|
36
|
+
'',
|
|
37
|
+
' git clone returned: Permission denied (publickey)',
|
|
38
|
+
' Tip: Ensure your SSH key or git credentials have access to this repo.',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatCircularDep(chain) {
|
|
43
|
+
return [
|
|
44
|
+
'error: circular dependency detected',
|
|
45
|
+
'',
|
|
46
|
+
` ${chain.join(' \u2192 ')}`,
|
|
47
|
+
'',
|
|
48
|
+
' Tova does not allow circular module dependencies.',
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatIntegrityError(modulePath, version, expectedSha, actualSha) {
|
|
53
|
+
return [
|
|
54
|
+
`error: integrity check failed for ${modulePath}@v${version}`,
|
|
55
|
+
'',
|
|
56
|
+
` Expected SHA: ${expectedSha}`,
|
|
57
|
+
` Got SHA: ${actualSha}`,
|
|
58
|
+
'',
|
|
59
|
+
' The git tag may have been force-pushed. This could indicate tampering.',
|
|
60
|
+
` Run \`tova update ${modulePath}\` to re-resolve.`,
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
package/src/config/resolve.js
CHANGED
|
@@ -73,6 +73,23 @@ function normalizeConfig(parsed, source) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// [package] section: marks this as a publishable package
|
|
77
|
+
if (parsed.package) {
|
|
78
|
+
config.package = {
|
|
79
|
+
name: parsed.package.name || '',
|
|
80
|
+
version: parsed.package.version || '0.1.0',
|
|
81
|
+
description: parsed.package.description || '',
|
|
82
|
+
license: parsed.package.license || '',
|
|
83
|
+
keywords: parsed.package.keywords || [],
|
|
84
|
+
homepage: parsed.package.homepage || '',
|
|
85
|
+
exports: parsed.package.exports || null,
|
|
86
|
+
entry: parsed.package.entry || null,
|
|
87
|
+
};
|
|
88
|
+
config.isPackage = true;
|
|
89
|
+
} else {
|
|
90
|
+
config.isPackage = false;
|
|
91
|
+
}
|
|
92
|
+
|
|
76
93
|
return config;
|
|
77
94
|
}
|
|
78
95
|
|