tova 0.7.0 → 0.9.4
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 +1312 -139
- package/package.json +8 -1
- package/src/analyzer/analyzer.js +539 -11
- package/src/analyzer/browser-analyzer.js +56 -8
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/scope.js +7 -0
- package/src/analyzer/server-analyzer.js +33 -1
- package/src/codegen/base-codegen.js +1296 -23
- package/src/codegen/browser-codegen.js +725 -20
- package/src/codegen/codegen.js +87 -5
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/server-codegen.js +54 -6
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/theme-codegen.js +69 -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 +63 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +26 -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 +61 -6
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +315 -0
- package/src/diagnostics/security-scorecard.js +111 -0
- package/src/lexer/lexer.js +18 -3
- package/src/lsp/server.js +482 -0
- package/src/parser/animate-ast.js +45 -0
- package/src/parser/ast.js +39 -0
- package/src/parser/browser-ast.js +19 -1
- package/src/parser/browser-parser.js +221 -4
- 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 +42 -5
- package/src/parser/select-ast.js +39 -0
- package/src/parser/theme-ast.js +29 -0
- package/src/parser/theme-parser.js +70 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/theme-plugin.js +20 -0
- package/src/registry/register-all.js +6 -0
- package/src/runtime/charts.js +547 -0
- package/src/runtime/embedded.js +6 -2
- package/src/runtime/reactivity.js +60 -0
- package/src/runtime/router.js +703 -295
- package/src/runtime/table.js +606 -33
- package/src/stdlib/inline.js +365 -10
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/stdlib/string.js +84 -2
- package/src/stdlib/validation.js +1 -1
- package/src/version.js +1 -1
|
@@ -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,63 @@
|
|
|
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
|
+
// Blessed first-party packages: tova/X → github.com/tova-lang/X
|
|
6
|
+
export const BLESSED_PACKAGES = {
|
|
7
|
+
fp: 'github.com/tova-lang/fp',
|
|
8
|
+
validate: 'github.com/tova-lang/validate',
|
|
9
|
+
encoding: 'github.com/tova-lang/encoding',
|
|
10
|
+
test: 'github.com/tova-lang/test',
|
|
11
|
+
retry: 'github.com/tova-lang/retry',
|
|
12
|
+
template: 'github.com/tova-lang/template',
|
|
13
|
+
data: 'github.com/tova-lang/data',
|
|
14
|
+
stats: 'github.com/tova-lang/stats',
|
|
15
|
+
plot: 'github.com/tova-lang/plot',
|
|
16
|
+
ml: 'github.com/tova-lang/ml',
|
|
17
|
+
ui: 'github.com/tova-lang/ui',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function expandBlessedPackage(source) {
|
|
21
|
+
if (!source || !source.startsWith('tova/')) return null;
|
|
22
|
+
const rest = source.slice(5);
|
|
23
|
+
const name = rest.split('/')[0];
|
|
24
|
+
if (BLESSED_PACKAGES[name]) return BLESSED_PACKAGES[name] + (rest.includes('/') ? '/' + rest.slice(name.length + 1) : '');
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isTovModule(source) {
|
|
29
|
+
if (!source || source.startsWith('.') || source.startsWith('/') || source.startsWith('@') || source.includes(':')) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
// Check blessed packages first
|
|
33
|
+
if (source.startsWith('tova/')) {
|
|
34
|
+
const name = source.slice(5).split('/')[0];
|
|
35
|
+
return !!BLESSED_PACKAGES[name];
|
|
36
|
+
}
|
|
37
|
+
const firstSegment = source.split('/')[0];
|
|
38
|
+
return firstSegment.includes('.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseModulePath(source) {
|
|
42
|
+
// Expand blessed packages: tova/data → github.com/tova-lang/data
|
|
43
|
+
const expanded = expandBlessedPackage(source);
|
|
44
|
+
const actual = expanded || source;
|
|
45
|
+
|
|
46
|
+
if (!expanded && !isTovModule(actual)) {
|
|
47
|
+
throw new Error(`Invalid Tova module path: "${source}"`);
|
|
48
|
+
}
|
|
49
|
+
const parts = actual.split('/');
|
|
50
|
+
if (parts.length < 3) {
|
|
51
|
+
throw new Error(`Invalid Tova module path: "${source}" — expected at least host/owner/repo`);
|
|
52
|
+
}
|
|
53
|
+
const host = parts[0];
|
|
54
|
+
const owner = parts[1];
|
|
55
|
+
const repo = parts[2];
|
|
56
|
+
const subpath = parts.length > 3 ? parts.slice(3).join('/') : null;
|
|
57
|
+
return { host, owner, repo, subpath, full: `${host}/${owner}/${repo}` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function moduleToGitUrl(modulePath) {
|
|
61
|
+
const parsed = typeof modulePath === 'string' ? parseModulePath(modulePath) : modulePath;
|
|
62
|
+
return `https://${parsed.full}.git`;
|
|
63
|
+
}
|
|
@@ -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
|
@@ -14,6 +14,9 @@ const DEFAULTS = {
|
|
|
14
14
|
build: {
|
|
15
15
|
output: '.tova-out',
|
|
16
16
|
},
|
|
17
|
+
deploy: {
|
|
18
|
+
base: '/',
|
|
19
|
+
},
|
|
17
20
|
dev: {
|
|
18
21
|
port: 3000,
|
|
19
22
|
},
|
|
@@ -53,6 +56,9 @@ function normalizeConfig(parsed, source) {
|
|
|
53
56
|
build: {
|
|
54
57
|
output: parsed.build?.output || DEFAULTS.build.output,
|
|
55
58
|
},
|
|
59
|
+
deploy: {
|
|
60
|
+
base: parsed.deploy?.base || DEFAULTS.deploy.base,
|
|
61
|
+
},
|
|
56
62
|
dev: {
|
|
57
63
|
port: parsed.dev?.port ?? DEFAULTS.dev.port,
|
|
58
64
|
},
|
|
@@ -73,6 +79,23 @@ function normalizeConfig(parsed, source) {
|
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
// [package] section: marks this as a publishable package
|
|
83
|
+
if (parsed.package) {
|
|
84
|
+
config.package = {
|
|
85
|
+
name: parsed.package.name || '',
|
|
86
|
+
version: parsed.package.version || '0.1.0',
|
|
87
|
+
description: parsed.package.description || '',
|
|
88
|
+
license: parsed.package.license || '',
|
|
89
|
+
keywords: parsed.package.keywords || [],
|
|
90
|
+
homepage: parsed.package.homepage || '',
|
|
91
|
+
exports: parsed.package.exports || null,
|
|
92
|
+
entry: parsed.package.entry || null,
|
|
93
|
+
};
|
|
94
|
+
config.isPackage = true;
|
|
95
|
+
} else {
|
|
96
|
+
config.isPackage = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
return config;
|
|
77
100
|
}
|
|
78
101
|
|
|
@@ -87,6 +110,9 @@ function configFromPackageJson(pkg) {
|
|
|
87
110
|
build: {
|
|
88
111
|
output: DEFAULTS.build.output,
|
|
89
112
|
},
|
|
113
|
+
deploy: {
|
|
114
|
+
base: '/',
|
|
115
|
+
},
|
|
90
116
|
dev: {
|
|
91
117
|
port: DEFAULTS.dev.port,
|
|
92
118
|
},
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/config/resolver.js
|
|
2
|
+
import { selectMinVersion, satisfies, parseSemver, compareSemver } from './semver.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merges dependency maps from multiple sources, collecting all constraints per module.
|
|
6
|
+
*/
|
|
7
|
+
export function mergeDependencies(...depMaps) {
|
|
8
|
+
const merged = {};
|
|
9
|
+
for (const deps of depMaps) {
|
|
10
|
+
for (const [mod, constraint] of Object.entries(deps)) {
|
|
11
|
+
if (!merged[mod]) merged[mod] = [];
|
|
12
|
+
if (Array.isArray(constraint)) {
|
|
13
|
+
merged[mod].push(...constraint);
|
|
14
|
+
} else {
|
|
15
|
+
merged[mod].push(constraint);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return merged;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Merges npm dependencies from multiple module configs.
|
|
24
|
+
* For conflicts, picks the highest constraint version.
|
|
25
|
+
*/
|
|
26
|
+
export function mergeNpmDeps(moduleConfigs) {
|
|
27
|
+
const merged = {};
|
|
28
|
+
for (const config of moduleConfigs) {
|
|
29
|
+
const prod = config.npm?.prod || {};
|
|
30
|
+
for (const [name, version] of Object.entries(prod)) {
|
|
31
|
+
if (!merged[name]) {
|
|
32
|
+
merged[name] = version;
|
|
33
|
+
} else {
|
|
34
|
+
// Keep whichever specifies a higher minimum
|
|
35
|
+
try {
|
|
36
|
+
const existing = parseSemver(merged[name].replace(/^[\^~>=<]*/, ''));
|
|
37
|
+
const incoming = parseSemver(version.replace(/^[\^~>=<]*/, ''));
|
|
38
|
+
if (compareSemver(incoming, existing) > 0) {
|
|
39
|
+
merged[name] = version;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
merged[name] = version;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return merged;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detects version conflicts — modules where no single version satisfies all constraints.
|
|
52
|
+
*/
|
|
53
|
+
export function detectConflicts(constraintMap, availableVersions) {
|
|
54
|
+
const conflicts = [];
|
|
55
|
+
for (const [mod, constraints] of Object.entries(constraintMap)) {
|
|
56
|
+
const versions = availableVersions[mod] || [];
|
|
57
|
+
const resolved = selectMinVersion(versions, constraints);
|
|
58
|
+
if (resolved === null && constraints.length > 1) {
|
|
59
|
+
conflicts.push({
|
|
60
|
+
module: mod,
|
|
61
|
+
constraints,
|
|
62
|
+
available: versions,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return conflicts;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolves all dependencies to exact versions.
|
|
71
|
+
* Returns a map of modulePath -> { version, sha, source, npmDeps }.
|
|
72
|
+
*
|
|
73
|
+
* This is the high-level orchestrator called by `tova install`.
|
|
74
|
+
* It takes callbacks for I/O operations (fetching tags, reading configs)
|
|
75
|
+
* so the core logic is testable without network access.
|
|
76
|
+
*/
|
|
77
|
+
export async function resolveDependencies(rootDeps, options = {}) {
|
|
78
|
+
const {
|
|
79
|
+
getAvailableVersions, // async (modulePath) => ['1.0.0', '1.1.0', ...]
|
|
80
|
+
getModuleConfig, // async (modulePath, version) => { dependencies, npm }
|
|
81
|
+
getVersionSha, // async (modulePath, version) => 'sha...'
|
|
82
|
+
} = options;
|
|
83
|
+
|
|
84
|
+
const resolved = {}; // modulePath -> { version, sha }
|
|
85
|
+
const allConstraints = {}; // modulePath -> [constraints...]
|
|
86
|
+
const allNpmDeps = []; // [{ npm: { prod: {...} } }, ...]
|
|
87
|
+
const queue = [rootDeps]; // queue of dependency maps to process
|
|
88
|
+
|
|
89
|
+
while (queue.length > 0) {
|
|
90
|
+
const deps = queue.shift();
|
|
91
|
+
for (const [mod, constraint] of Object.entries(deps)) {
|
|
92
|
+
if (!allConstraints[mod]) allConstraints[mod] = [];
|
|
93
|
+
allConstraints[mod].push(constraint);
|
|
94
|
+
|
|
95
|
+
// Get available versions
|
|
96
|
+
const versions = await getAvailableVersions(mod);
|
|
97
|
+
const version = selectMinVersion(versions, allConstraints[mod]);
|
|
98
|
+
|
|
99
|
+
if (version === null) {
|
|
100
|
+
const conflicts = detectConflicts(
|
|
101
|
+
{ [mod]: allConstraints[mod] },
|
|
102
|
+
{ [mod]: versions }
|
|
103
|
+
);
|
|
104
|
+
if (conflicts.length > 0) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Version conflict for ${mod}:\n` +
|
|
107
|
+
conflicts[0].constraints.map(c => ` requires ${c}`).join('\n') +
|
|
108
|
+
`\n Available: ${versions.join(', ')}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
throw new Error(
|
|
112
|
+
`No version of ${mod} satisfies constraint: ${allConstraints[mod].join(', ')}\n` +
|
|
113
|
+
` Available: ${versions.join(', ') || 'none'}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Skip if we already resolved this module to the same or higher version
|
|
118
|
+
if (resolved[mod] && compareSemver(resolved[mod].version, version) >= 0) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sha = await getVersionSha(mod, version);
|
|
123
|
+
resolved[mod] = { version, sha, source: `https://${mod}.git` };
|
|
124
|
+
|
|
125
|
+
// Read this module's config for transitive deps
|
|
126
|
+
const config = await getModuleConfig(mod, version);
|
|
127
|
+
if (config) {
|
|
128
|
+
allNpmDeps.push(config);
|
|
129
|
+
if (config.dependencies && Object.keys(config.dependencies).length > 0) {
|
|
130
|
+
queue.push(config.dependencies);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const npmDeps = mergeNpmDeps(allNpmDeps);
|
|
137
|
+
|
|
138
|
+
return { resolved, npmDeps };
|
|
139
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/config/search.js
|
|
2
|
+
|
|
3
|
+
export function formatSearchResults(items) {
|
|
4
|
+
if (items.length === 0) return ' No packages found.\n';
|
|
5
|
+
const lines = [];
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
const modulePath = `github.com/${item.full_name}`;
|
|
8
|
+
const stars = item.stargazers_count || 0;
|
|
9
|
+
const desc = item.description || '(no description)';
|
|
10
|
+
const updated = item.updated_at ? item.updated_at.slice(0, 10) : 'unknown';
|
|
11
|
+
lines.push(` ${modulePath}`);
|
|
12
|
+
lines.push(` ${desc}`);
|
|
13
|
+
lines.push(` Stars: ${stars} Updated: ${updated}`);
|
|
14
|
+
lines.push('');
|
|
15
|
+
}
|
|
16
|
+
return lines.join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function searchPackages(query) {
|
|
20
|
+
const searchQuery = encodeURIComponent(`${query} topic:tova-package`);
|
|
21
|
+
const url = `https://api.github.com/search/repositories?q=${searchQuery}&sort=stars&per_page=20`;
|
|
22
|
+
const res = await fetch(url, {
|
|
23
|
+
headers: { 'Accept': 'application/vnd.github.v3+json' },
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) throw new Error(`GitHub search failed: ${res.statusText}`);
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
return data.items || [];
|
|
28
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/config/semver.js
|
|
2
|
+
// Semver utilities for the Tova package manager.
|
|
3
|
+
// Provides parsing, comparison, constraint satisfaction, and minimum version selection.
|
|
4
|
+
|
|
5
|
+
export function parseSemver(str) {
|
|
6
|
+
const s = str.startsWith('v') ? str.slice(1) : str;
|
|
7
|
+
const parts = s.split('.');
|
|
8
|
+
const major = parseInt(parts[0], 10);
|
|
9
|
+
const minor = parseInt(parts[1] || '0', 10);
|
|
10
|
+
const patch = parseInt(parts[2] || '0', 10);
|
|
11
|
+
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
|
|
12
|
+
throw new Error(`Invalid semver: "${str}"`);
|
|
13
|
+
}
|
|
14
|
+
return { major, minor, patch };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function compareSemver(a, b) {
|
|
18
|
+
const va = typeof a === 'string' ? parseSemver(a) : a;
|
|
19
|
+
const vb = typeof b === 'string' ? parseSemver(b) : b;
|
|
20
|
+
if (va.major !== vb.major) return va.major > vb.major ? 1 : -1;
|
|
21
|
+
if (va.minor !== vb.minor) return va.minor > vb.minor ? 1 : -1;
|
|
22
|
+
if (va.patch !== vb.patch) return va.patch > vb.patch ? 1 : -1;
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseConstraint(constraint) {
|
|
27
|
+
if (constraint.startsWith('^')) {
|
|
28
|
+
return { type: 'caret', version: parseSemver(constraint.slice(1)) };
|
|
29
|
+
}
|
|
30
|
+
if (constraint.startsWith('~')) {
|
|
31
|
+
return { type: 'tilde', version: parseSemver(constraint.slice(1)) };
|
|
32
|
+
}
|
|
33
|
+
if (constraint.startsWith('>=')) {
|
|
34
|
+
return { type: 'gte', version: parseSemver(constraint.slice(2)) };
|
|
35
|
+
}
|
|
36
|
+
if (constraint.startsWith('>')) {
|
|
37
|
+
return { type: 'gt', version: parseSemver(constraint.slice(1)) };
|
|
38
|
+
}
|
|
39
|
+
return { type: 'exact', version: parseSemver(constraint) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function satisfies(version, constraint) {
|
|
43
|
+
const v = typeof version === 'string' ? parseSemver(version) : version;
|
|
44
|
+
const c = typeof constraint === 'string' ? parseConstraint(constraint) : constraint;
|
|
45
|
+
switch (c.type) {
|
|
46
|
+
case 'exact':
|
|
47
|
+
return compareSemver(v, c.version) === 0;
|
|
48
|
+
case 'caret':
|
|
49
|
+
if (compareSemver(v, c.version) < 0) return false;
|
|
50
|
+
return v.major === c.version.major;
|
|
51
|
+
case 'tilde':
|
|
52
|
+
if (compareSemver(v, c.version) < 0) return false;
|
|
53
|
+
return v.major === c.version.major && v.minor === c.version.minor;
|
|
54
|
+
case 'gte':
|
|
55
|
+
return compareSemver(v, c.version) >= 0;
|
|
56
|
+
case 'gt':
|
|
57
|
+
return compareSemver(v, c.version) > 0;
|
|
58
|
+
default:
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function selectMinVersion(versions, constraints) {
|
|
64
|
+
const constraintList = Array.isArray(constraints) ? constraints : [constraints];
|
|
65
|
+
const sorted = [...versions].sort((a, b) => compareSemver(a, b));
|
|
66
|
+
for (const ver of sorted) {
|
|
67
|
+
if (constraintList.every(c => satisfies(ver, c))) {
|
|
68
|
+
return ver;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
package/src/config/toml.js
CHANGED
|
@@ -13,10 +13,10 @@ export function parseTOML(input) {
|
|
|
13
13
|
// Skip empty lines and comments
|
|
14
14
|
if (line === '' || line.startsWith('#')) continue;
|
|
15
15
|
|
|
16
|
-
// Section header: [section] or [section.subsection]
|
|
16
|
+
// Section header: [section] or [section.subsection] or ["quoted.key"]
|
|
17
17
|
if (line.startsWith('[') && !line.startsWith('[[')) {
|
|
18
|
-
const close = line.
|
|
19
|
-
if (close
|
|
18
|
+
const close = line.lastIndexOf(']');
|
|
19
|
+
if (close <= 0) {
|
|
20
20
|
throw new Error(`TOML parse error on line ${i + 1}: unclosed section header`);
|
|
21
21
|
}
|
|
22
22
|
const sectionPath = line.slice(1, close).trim();
|
|
@@ -24,8 +24,8 @@ export function parseTOML(input) {
|
|
|
24
24
|
throw new Error(`TOML parse error on line ${i + 1}: empty section name`);
|
|
25
25
|
}
|
|
26
26
|
current = result;
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const parts = parseSectionPath(sectionPath);
|
|
28
|
+
for (const key of parts) {
|
|
29
29
|
if (!current[key]) current[key] = {};
|
|
30
30
|
current = current[key];
|
|
31
31
|
}
|
|
@@ -37,7 +37,19 @@ export function parseTOML(input) {
|
|
|
37
37
|
if (eqIdx === -1) continue; // skip lines without =
|
|
38
38
|
|
|
39
39
|
const key = line.slice(0, eqIdx).trim();
|
|
40
|
-
|
|
40
|
+
let rawValue = line.slice(eqIdx + 1).trim();
|
|
41
|
+
|
|
42
|
+
// Multi-line array: starts with [ but doesn't end with ]
|
|
43
|
+
if (rawValue.startsWith('[') && !rawValue.endsWith(']')) {
|
|
44
|
+
// Collect continuation lines until we find the closing ]
|
|
45
|
+
while (i + 1 < lines.length) {
|
|
46
|
+
i++;
|
|
47
|
+
const contLine = lines[i].trim();
|
|
48
|
+
if (contLine === '' || contLine.startsWith('#')) continue;
|
|
49
|
+
rawValue += ' ' + contLine;
|
|
50
|
+
if (contLine.includes(']')) break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
41
53
|
|
|
42
54
|
current[key] = parseValue(rawValue, i + 1);
|
|
43
55
|
}
|
|
@@ -45,6 +57,49 @@ export function parseTOML(input) {
|
|
|
45
57
|
return result;
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
function parseSectionPath(path) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < path.length) {
|
|
64
|
+
// Skip whitespace
|
|
65
|
+
while (i < path.length && path[i] === ' ') i++;
|
|
66
|
+
if (i >= path.length) break;
|
|
67
|
+
|
|
68
|
+
if (path[i] === '"') {
|
|
69
|
+
// Quoted key: read until closing quote
|
|
70
|
+
i++; // skip opening quote
|
|
71
|
+
let key = '';
|
|
72
|
+
while (i < path.length && path[i] !== '"') {
|
|
73
|
+
if (path[i] === '\\') {
|
|
74
|
+
i++;
|
|
75
|
+
key += path[i] || '';
|
|
76
|
+
} else {
|
|
77
|
+
key += path[i];
|
|
78
|
+
}
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
i++; // skip closing quote
|
|
82
|
+
parts.push(key);
|
|
83
|
+
} else {
|
|
84
|
+
// Bare key: read until dot or end
|
|
85
|
+
let key = '';
|
|
86
|
+
while (i < path.length && path[i] !== '.') {
|
|
87
|
+
key += path[i];
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
key = key.trim();
|
|
91
|
+
if (key) parts.push(key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Skip dot separator
|
|
95
|
+
while (i < path.length && (path[i] === ' ' || path[i] === '.')) {
|
|
96
|
+
if (path[i] === '.') { i++; break; }
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return parts;
|
|
101
|
+
}
|
|
102
|
+
|
|
48
103
|
function parseValue(raw, lineNum) {
|
|
49
104
|
if (raw === '') {
|
|
50
105
|
throw new Error(`TOML parse error on line ${lineNum}: missing value`);
|