@venn-lang/contracts 0.1.0
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/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/index.d.ts +620 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1342 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.mts +287 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +387 -0
- package/dist/node.mjs.map +1 -0
- package/dist/signal-source.types-BI3TYaCi.d.ts +317 -0
- package/dist/signal-source.types-BI3TYaCi.d.ts.map +1 -0
- package/dist/testing.d.ts +84 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +276 -0
- package/dist/testing.js.map +1 -0
- package/package.json +77 -0
- package/src/capabilities/host-capability.ts +18 -0
- package/src/capabilities/index.ts +2 -0
- package/src/conformance/expect-venn-error.ts +18 -0
- package/src/conformance/harness.types.ts +14 -0
- package/src/conformance/index.ts +2 -0
- package/src/dotenv/dotenv-files.ts +30 -0
- package/src/dotenv/index.ts +2 -0
- package/src/dotenv/parse-dotenv.ts +34 -0
- package/src/errors/contract-codes.ts +37 -0
- package/src/errors/index.ts +7 -0
- package/src/errors/venn-error.ts +20 -0
- package/src/host/create-host.ts +16 -0
- package/src/host/create-node-host.ts +29 -0
- package/src/host/create-test-host.ts +28 -0
- package/src/host/create-worker-host.ts +32 -0
- package/src/host/host.types.ts +26 -0
- package/src/host/index.ts +5 -0
- package/src/host/unavailable.ts +23 -0
- package/src/index.ts +10 -0
- package/src/logger/console-logger.ts +12 -0
- package/src/logger/index.ts +3 -0
- package/src/logger/logger.types.ts +18 -0
- package/src/logger/memory-logger.ts +12 -0
- package/src/node.ts +8 -0
- package/src/port/assert-capabilities.ts +18 -0
- package/src/port/assert-port-shape.ts +18 -0
- package/src/port/bind-port.ts +28 -0
- package/src/port/index.ts +5 -0
- package/src/port/missing-capabilities.ts +14 -0
- package/src/port/port.types.ts +36 -0
- package/src/ports/clock/clock.port.ts +15 -0
- package/src/ports/clock/clock.types.ts +17 -0
- package/src/ports/clock/index.ts +4 -0
- package/src/ports/clock/system-clock.ts +9 -0
- package/src/ports/clock/virtual-clock.ts +22 -0
- package/src/ports/console/console.port.ts +13 -0
- package/src/ports/console/console.types.ts +17 -0
- package/src/ports/console/index.ts +3 -0
- package/src/ports/console/memory-console.ts +36 -0
- package/src/ports/console/node-console.ts +80 -0
- package/src/ports/file-system/file-system.errors.ts +10 -0
- package/src/ports/file-system/file-system.port.ts +12 -0
- package/src/ports/file-system/file-system.types.ts +27 -0
- package/src/ports/file-system/index.ts +6 -0
- package/src/ports/file-system/memory-fs.ts +46 -0
- package/src/ports/file-system/node-fs.ts +93 -0
- package/src/ports/index.ts +9 -0
- package/src/ports/lock-provider/fake-lock.ts +6 -0
- package/src/ports/lock-provider/in-process-lock.ts +26 -0
- package/src/ports/lock-provider/index.ts +4 -0
- package/src/ports/lock-provider/lock-provider.port.ts +16 -0
- package/src/ports/lock-provider/lock-provider.types.ts +8 -0
- package/src/ports/manifest-provider/default-manifest.ts +31 -0
- package/src/ports/manifest-provider/edit/edit-dependency.ts +82 -0
- package/src/ports/manifest-provider/edit/index.ts +7 -0
- package/src/ports/manifest-provider/edit/table-span.ts +39 -0
- package/src/ports/manifest-provider/index.ts +31 -0
- package/src/ports/manifest-provider/manifest.port.ts +16 -0
- package/src/ports/manifest-provider/manifest.types.ts +54 -0
- package/src/ports/manifest-provider/memory-manifest.ts +14 -0
- package/src/ports/manifest-provider/project.types.ts +94 -0
- package/src/ports/manifest-provider/read/index.ts +15 -0
- package/src/ports/manifest-provider/read/read-dependencies.ts +27 -0
- package/src/ports/manifest-provider/read/read-package.ts +32 -0
- package/src/ports/manifest-provider/read/read-profiles.ts +32 -0
- package/src/ports/manifest-provider/read/read-targets.ts +37 -0
- package/src/ports/manifest-provider/read/read-tooling.ts +15 -0
- package/src/ports/manifest-provider/read/read-workspace.ts +22 -0
- package/src/ports/manifest-provider/read/scalars.ts +35 -0
- package/src/ports/manifest-provider/read-run-settings.ts +38 -0
- package/src/ports/manifest-provider/resolve-alias.ts +22 -0
- package/src/ports/manifest-provider/toml/index.ts +3 -0
- package/src/ports/manifest-provider/toml/parse-toml.ts +52 -0
- package/src/ports/manifest-provider/toml/read-value.ts +104 -0
- package/src/ports/manifest-provider/toml/sections.ts +39 -0
- package/src/ports/manifest-provider/toml-docs.ts +27 -0
- package/src/ports/manifest-provider/toml-manifest.ts +41 -0
- package/src/ports/process-provider/fake-process.ts +32 -0
- package/src/ports/process-provider/index.ts +10 -0
- package/src/ports/process-provider/node-spawn.ts +52 -0
- package/src/ports/process-provider/process-provider.port.ts +13 -0
- package/src/ports/process-provider/process-provider.types.ts +47 -0
- package/src/ports/random/fixed-random.ts +10 -0
- package/src/ports/random/index.ts +4 -0
- package/src/ports/random/random.port.ts +13 -0
- package/src/ports/random/random.types.ts +10 -0
- package/src/ports/random/seeded-random.ts +19 -0
- package/src/ports/secret-provider/env-secrets.ts +19 -0
- package/src/ports/secret-provider/index.ts +6 -0
- package/src/ports/secret-provider/memory-secrets.ts +15 -0
- package/src/ports/secret-provider/secret-provider.port.ts +13 -0
- package/src/ports/secret-provider/secret-provider.types.ts +17 -0
- package/src/ports/secret-provider/secret.ts +17 -0
- package/src/ports/signal-source/fake-signals.ts +44 -0
- package/src/ports/signal-source/index.ts +11 -0
- package/src/ports/signal-source/node-signals.ts +33 -0
- package/src/ports/signal-source/signal-source.port.ts +13 -0
- package/src/ports/signal-source/signal-source.types.ts +24 -0
- package/src/testing.ts +16 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { keyOf, tableSpan } from "./table-span.js";
|
|
2
|
+
|
|
3
|
+
/** What to change, and in which table. */
|
|
4
|
+
export interface DependencyEdit {
|
|
5
|
+
/** The manifest as written. */
|
|
6
|
+
text: string;
|
|
7
|
+
name: string;
|
|
8
|
+
/** Defaults to {@link DEPENDENCIES}. Pass `"dev-dependencies"` for the other. */
|
|
9
|
+
table?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The table an edit touches unless told otherwise. */
|
|
13
|
+
export const DEPENDENCIES = "dependencies";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The manifest with one dependency written in, everything else untouched.
|
|
17
|
+
*
|
|
18
|
+
* Entries are kept in name order, so two people adding two packages produce two
|
|
19
|
+
* one-line diffs rather than a conflict. A name already there is replaced where
|
|
20
|
+
* it stands, because an upgrade should not move the line.
|
|
21
|
+
*
|
|
22
|
+
* @returns the whole manifest as text. A missing table is appended at the end.
|
|
23
|
+
*/
|
|
24
|
+
export function addDependency(args: DependencyEdit & { version: string }): string {
|
|
25
|
+
const table = args.table ?? DEPENDENCIES;
|
|
26
|
+
const lines = args.text.split("\n");
|
|
27
|
+
const entry = `${args.name} = "${args.version}"`;
|
|
28
|
+
const span = tableSpan(lines, table);
|
|
29
|
+
if (!span) return appended(args.text, table, entry);
|
|
30
|
+
const at = lines.findIndex((line, index) => inside(span, index) && keyOf(line) === args.name);
|
|
31
|
+
if (at >= 0) return replaced(lines, at, entry);
|
|
32
|
+
return replaced(lines, insertionPoint(lines, span, args.name), entry, "insert");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The manifest without that dependency.
|
|
37
|
+
*
|
|
38
|
+
* @returns the text unchanged when the name is not there: absent is already the
|
|
39
|
+
* wanted state.
|
|
40
|
+
*/
|
|
41
|
+
export function removeDependency(args: DependencyEdit): string {
|
|
42
|
+
const table = args.table ?? DEPENDENCIES;
|
|
43
|
+
const lines = args.text.split("\n");
|
|
44
|
+
const span = tableSpan(lines, table);
|
|
45
|
+
if (!span) return args.text;
|
|
46
|
+
const at = lines.findIndex((line, index) => inside(span, index) && keyOf(line) === args.name);
|
|
47
|
+
if (at < 0) return args.text;
|
|
48
|
+
return [...lines.slice(0, at), ...lines.slice(at + 1)].join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inside(span: { from: number; to: number }, index: number): boolean {
|
|
52
|
+
return index >= span.from && index < span.to;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Where the name belongs, in order, among the entries already there. */
|
|
56
|
+
function insertionPoint(
|
|
57
|
+
lines: readonly string[],
|
|
58
|
+
span: { from: number; to: number },
|
|
59
|
+
name: string,
|
|
60
|
+
): number {
|
|
61
|
+
for (let at = span.from; at < span.to; at++) {
|
|
62
|
+
const key = keyOf(lines[at] ?? "");
|
|
63
|
+
if (key && key > name) return at;
|
|
64
|
+
}
|
|
65
|
+
return span.to;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function replaced(
|
|
69
|
+
lines: readonly string[],
|
|
70
|
+
at: number,
|
|
71
|
+
entry: string,
|
|
72
|
+
how: "replace" | "insert" = "replace",
|
|
73
|
+
): string {
|
|
74
|
+
const after = how === "insert" ? lines.slice(at) : lines.slice(at + 1);
|
|
75
|
+
return [...lines.slice(0, at), entry, ...after].join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A table the manifest does not have yet, written at the end where it reads. */
|
|
79
|
+
function appended(text: string, table: string, entry: string): string {
|
|
80
|
+
const body = text.endsWith("\n") ? text : `${text}\n`;
|
|
81
|
+
return `${body}\n[${table}]\n${entry}\n`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Where a table's entries live in a manifest's lines. */
|
|
2
|
+
export interface TableSpan {
|
|
3
|
+
/** The line holding `[name]`. */
|
|
4
|
+
header: number;
|
|
5
|
+
/** First line after the header, and the line after the table's last entry. */
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Where a `[table]` is written, or undefined when the file has none.
|
|
12
|
+
*
|
|
13
|
+
* Found by reading lines rather than by re-serialising the parse tree. A
|
|
14
|
+
* manifest carries its author's comments, blank lines and chosen order, and
|
|
15
|
+
* rebuilding it from a tree would throw all of that away.
|
|
16
|
+
*/
|
|
17
|
+
export function tableSpan(lines: readonly string[], name: string): TableSpan | undefined {
|
|
18
|
+
const header = lines.findIndex((line) => line.trim() === `[${name}]`);
|
|
19
|
+
if (header < 0) return undefined;
|
|
20
|
+
let to = header + 1;
|
|
21
|
+
for (let at = header + 1; at < lines.length; at++) {
|
|
22
|
+
if (lines[at]?.trimStart().startsWith("[")) break;
|
|
23
|
+
if (lines[at]?.trim() !== "") to = at + 1;
|
|
24
|
+
}
|
|
25
|
+
return { header, from: header + 1, to };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The key a `key = value` line writes, or undefined for a comment or a blank. */
|
|
29
|
+
export function keyOf(line: string): string | undefined {
|
|
30
|
+
const text = line.trim();
|
|
31
|
+
if (text === "" || text.startsWith("#") || text.startsWith("[")) return undefined;
|
|
32
|
+
const equals = text.indexOf("=");
|
|
33
|
+
return equals < 0
|
|
34
|
+
? undefined
|
|
35
|
+
: text
|
|
36
|
+
.slice(0, equals)
|
|
37
|
+
.trim()
|
|
38
|
+
.replace(/^["']|["']$/g, "");
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { defaultManifest } from "./default-manifest.js";
|
|
2
|
+
export {
|
|
3
|
+
addDependency,
|
|
4
|
+
DEPENDENCIES,
|
|
5
|
+
type DependencyEdit,
|
|
6
|
+
removeDependency,
|
|
7
|
+
} from "./edit/index.js";
|
|
8
|
+
export { ManifestProviderPort } from "./manifest.port.js";
|
|
9
|
+
export type { FormatSettings, Manifest, ManifestProvider } from "./manifest.types.js";
|
|
10
|
+
export { createMemoryManifest } from "./memory-manifest.js";
|
|
11
|
+
export type {
|
|
12
|
+
BuildTarget,
|
|
13
|
+
Dependency,
|
|
14
|
+
PackageInfo,
|
|
15
|
+
PackageManagerName,
|
|
16
|
+
Profile,
|
|
17
|
+
TargetKind,
|
|
18
|
+
ToolingSettings,
|
|
19
|
+
WorkspaceSettings,
|
|
20
|
+
} from "./project.types.js";
|
|
21
|
+
export {
|
|
22
|
+
BIN_DIR,
|
|
23
|
+
DEFAULT_PROFILES,
|
|
24
|
+
LIB_ROOT,
|
|
25
|
+
MAIN_ROOT,
|
|
26
|
+
readInheritable,
|
|
27
|
+
} from "./read/index.js";
|
|
28
|
+
export { type AliasTarget, resolveAlias } from "./resolve-alias.js";
|
|
29
|
+
export { parseToml } from "./toml/index.js";
|
|
30
|
+
export { tomlDocs } from "./toml-docs.js";
|
|
31
|
+
export { createTomlManifest } from "./toml-manifest.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Port } from "../../port/index.js";
|
|
2
|
+
import type { ManifestProvider } from "./manifest.types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The {@link ManifestProvider} contract. Implementations: `toml-manifest`,
|
|
6
|
+
* `memory-manifest`.
|
|
7
|
+
*
|
|
8
|
+
* Requires nothing: reading the file is the caller's job, and what arrives here
|
|
9
|
+
* is already text.
|
|
10
|
+
*/
|
|
11
|
+
export const ManifestProviderPort: Port<ManifestProvider> = {
|
|
12
|
+
id: "venn.port.manifest",
|
|
13
|
+
version: 1,
|
|
14
|
+
requires: [],
|
|
15
|
+
methods: ["load"],
|
|
16
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuildTarget,
|
|
3
|
+
Dependency,
|
|
4
|
+
PackageInfo,
|
|
5
|
+
Profile,
|
|
6
|
+
ToolingSettings,
|
|
7
|
+
WorkspaceSettings,
|
|
8
|
+
} from "./project.types.js";
|
|
9
|
+
|
|
10
|
+
/** A parsed `venn.toml`: the project manifest the LSP and the runner read. */
|
|
11
|
+
export interface Manifest {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
/** `[package]` in full. `name` and `version` above are the short way in. */
|
|
15
|
+
package: PackageInfo;
|
|
16
|
+
/** `[lib]` and `[[bin]]`, in the order they were written. */
|
|
17
|
+
targets: readonly BuildTarget[];
|
|
18
|
+
dependencies: readonly Dependency[];
|
|
19
|
+
devDependencies: readonly Dependency[];
|
|
20
|
+
/** `[patch]`: a version this project insists on, wherever it is asked for. */
|
|
21
|
+
patch: readonly Dependency[];
|
|
22
|
+
/** `[profile.<name>]`, keyed by name. `dev` and `release` are built in. */
|
|
23
|
+
profiles: Record<string, Profile>;
|
|
24
|
+
tooling: ToolingSettings;
|
|
25
|
+
/** `[workspace]`, or absent when this manifest is a plain package. */
|
|
26
|
+
workspace?: WorkspaceSettings;
|
|
27
|
+
/** `[env.<name>]` sections, mapping an environment to its variables. */
|
|
28
|
+
env: Record<string, Record<string, string>>;
|
|
29
|
+
/**
|
|
30
|
+
* `[env] files`: which dotenv files to read, in order, later winning.
|
|
31
|
+
* `${name}` stands for the selected environment. Empty means the convention.
|
|
32
|
+
*/
|
|
33
|
+
envFiles: readonly string[];
|
|
34
|
+
/** `[paths]`, mapping an `#alias` to a directory. */
|
|
35
|
+
paths: Record<string, string>;
|
|
36
|
+
/** `[format]`: how `venn fmt` and the editor format this project. */
|
|
37
|
+
format: FormatSettings;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The `[format]` table. Absent keys fall back to the language defaults. */
|
|
41
|
+
export interface FormatSettings {
|
|
42
|
+
indent?: number;
|
|
43
|
+
tabs?: boolean;
|
|
44
|
+
organize?: boolean;
|
|
45
|
+
sort?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Produces a {@link Manifest}. Implementations: `toml-manifest` parses one,
|
|
50
|
+
* `memory-manifest` returns a preset.
|
|
51
|
+
*/
|
|
52
|
+
export interface ManifestProvider {
|
|
53
|
+
load(): Manifest;
|
|
54
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defaultManifest } from "./default-manifest.js";
|
|
2
|
+
import type { Manifest, ManifestProvider } from "./manifest.types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The double: a preset manifest, with no file involved.
|
|
6
|
+
*
|
|
7
|
+
* Takes only what the caller cares about. The rest is what a project that
|
|
8
|
+
* declared nothing would get, which is exactly what the real provider gives a
|
|
9
|
+
* `venn.toml` silent on those tables.
|
|
10
|
+
*/
|
|
11
|
+
export function createMemoryManifest(args: { manifest: Partial<Manifest> }): ManifestProvider {
|
|
12
|
+
const manifest: Manifest = defaultManifest(args.manifest);
|
|
13
|
+
return { load: () => manifest };
|
|
14
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a manifest declares about the project itself, as against how it runs.
|
|
3
|
+
*
|
|
4
|
+
* The shape follows Cargo's, because the questions are the same and an answer
|
|
5
|
+
* the reader already knows beats a better one they must learn. Where Cargo has
|
|
6
|
+
* something this does not, the gap is noted so it reads as a decision.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** `[package]`: who this is. */
|
|
10
|
+
export interface PackageInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
/**
|
|
13
|
+
* What `[package] version` said, and nothing when it said nothing.
|
|
14
|
+
*
|
|
15
|
+
* Optional so "not written" stays distinguishable from "written as 0.0.0": a
|
|
16
|
+
* workspace member inherits the first and keeps the second. `Manifest.version`
|
|
17
|
+
* is the resolved value, with the default already applied.
|
|
18
|
+
*/
|
|
19
|
+
version?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
license?: string;
|
|
22
|
+
authors: readonly string[];
|
|
23
|
+
/** `[package] edition`: which language revision this was written against. */
|
|
24
|
+
edition?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What a project builds. A server is a `bin` that does not end. */
|
|
28
|
+
export type TargetKind = "lib" | "bin" | "test";
|
|
29
|
+
|
|
30
|
+
/** `[lib]` or one entry of `[[bin]]`: a thing to build, and where it starts. */
|
|
31
|
+
export interface BuildTarget {
|
|
32
|
+
kind: TargetKind;
|
|
33
|
+
/** The name it is known by; a `bin`'s name is what `venn run --bin` takes. */
|
|
34
|
+
name: string;
|
|
35
|
+
/** The `.vn` file this target starts from, relative to the manifest. */
|
|
36
|
+
path: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One entry of `[dependencies]` / `[dev-dependencies]`. */
|
|
40
|
+
export interface Dependency {
|
|
41
|
+
name: string;
|
|
42
|
+
/** A semver range, or absent when the version comes from elsewhere. */
|
|
43
|
+
version?: string;
|
|
44
|
+
/** `{ path = "../other" }`: a sibling in this workspace or on disk. */
|
|
45
|
+
path?: string;
|
|
46
|
+
/** `{ workspace = true }`: take the version the workspace pinned. */
|
|
47
|
+
fromWorkspace: boolean;
|
|
48
|
+
/** `{ optional = true }`: resolved and locked, installed only on demand. */
|
|
49
|
+
optional: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `[profile.dev]` and `[profile.release]`: how a build is made.
|
|
54
|
+
*
|
|
55
|
+
* One key, because one key is what a build does today. Settings about generated
|
|
56
|
+
* code, such as `sourceMap` and `minify`, arrive with the compiler: declaring
|
|
57
|
+
* them now would read as a contract and honour nothing.
|
|
58
|
+
*/
|
|
59
|
+
export interface Profile {
|
|
60
|
+
/** Whether a problem stops the build instead of only being reported. */
|
|
61
|
+
strict?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Which package manager runs underneath `venn add` and `venn remove`.
|
|
66
|
+
*
|
|
67
|
+
* The interface is ours, the resolution is not: resolving versions well is
|
|
68
|
+
* years of work against three moving targets.
|
|
69
|
+
*/
|
|
70
|
+
export type PackageManagerName = "pnpm" | "npm" | "bun" | "yarn";
|
|
71
|
+
|
|
72
|
+
/** The `[tooling]` table. */
|
|
73
|
+
export interface ToolingSettings {
|
|
74
|
+
manager: PackageManagerName;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `[workspace]`: a root that owns members, one lockfile and one `target/`.
|
|
79
|
+
*
|
|
80
|
+
* `[features]` is deliberately absent. It is the most intricate part of Cargo,
|
|
81
|
+
* it rests on conditional compilation this language does not have, and half of
|
|
82
|
+
* it would be worse than none.
|
|
83
|
+
*/
|
|
84
|
+
export interface WorkspaceSettings {
|
|
85
|
+
/** Path globs naming the members, e.g. `["packages/*"]`. */
|
|
86
|
+
members: readonly string[];
|
|
87
|
+
exclude: readonly string[];
|
|
88
|
+
/** Which members a command with no target acts on. Empty means all of them. */
|
|
89
|
+
defaultMembers: readonly string[];
|
|
90
|
+
/** `[workspace.package]`: metadata a member may inherit. */
|
|
91
|
+
package: Partial<PackageInfo>;
|
|
92
|
+
/** `[workspace.dependencies]`: the versions `{ workspace = true }` takes. */
|
|
93
|
+
dependencies: readonly Dependency[];
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { readDependencies } from "./read-dependencies.js";
|
|
2
|
+
export { readInheritable, readPackage } from "./read-package.js";
|
|
3
|
+
export { DEFAULT_PROFILES, readProfiles } from "./read-profiles.js";
|
|
4
|
+
export { BIN_DIR, LIB_ROOT, MAIN_ROOT, readTargets } from "./read-targets.js";
|
|
5
|
+
export { readTooling } from "./read-tooling.js";
|
|
6
|
+
export { readWorkspace } from "./read-workspace.js";
|
|
7
|
+
export {
|
|
8
|
+
asBoolean,
|
|
9
|
+
asList,
|
|
10
|
+
asNumber,
|
|
11
|
+
asRecord,
|
|
12
|
+
asRecords,
|
|
13
|
+
asString,
|
|
14
|
+
asStringMap,
|
|
15
|
+
} from "./scalars.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Dependency } from "../project.types.js";
|
|
2
|
+
import { asBoolean, asRecord, asString } from "./scalars.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A dependency table, in either spelling TOML allows.
|
|
6
|
+
*
|
|
7
|
+
* `zod = "^4"` and `zod = { version = "^4", optional = true }` say the same
|
|
8
|
+
* thing about the version, so both read into one shape rather than two the rest
|
|
9
|
+
* of the code would have to keep apart.
|
|
10
|
+
*/
|
|
11
|
+
export function readDependencies(value: unknown): Dependency[] {
|
|
12
|
+
return Object.entries(asRecord(value)).map(([name, spec]) => readOne(name, spec));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readOne(name: string, spec: unknown): Dependency {
|
|
16
|
+
if (typeof spec === "string") {
|
|
17
|
+
return { name, version: spec, fromWorkspace: false, optional: false };
|
|
18
|
+
}
|
|
19
|
+
const table = asRecord(spec);
|
|
20
|
+
return {
|
|
21
|
+
name,
|
|
22
|
+
version: asString(table.version),
|
|
23
|
+
path: asString(table.path),
|
|
24
|
+
fromWorkspace: asBoolean(table.workspace) ?? false,
|
|
25
|
+
optional: asBoolean(table.optional) ?? false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PackageInfo } from "../project.types.js";
|
|
2
|
+
import { asList, asRecord, asString } from "./scalars.js";
|
|
3
|
+
|
|
4
|
+
/** `[package]`: who this is. A missing table reads as a nameless package. */
|
|
5
|
+
export function readPackage(data: Record<string, unknown>): PackageInfo {
|
|
6
|
+
const table = asRecord(data.package);
|
|
7
|
+
return {
|
|
8
|
+
name: String(table.name ?? ""),
|
|
9
|
+
version: asString(table.version),
|
|
10
|
+
description: asString(table.description),
|
|
11
|
+
license: asString(table.license),
|
|
12
|
+
authors: asList(table.authors),
|
|
13
|
+
edition: asString(table.edition),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The same table, reduced to what a member may inherit from its workspace.
|
|
19
|
+
*
|
|
20
|
+
* A name is never inherited, being the one thing that must differ between two
|
|
21
|
+
* members, so `[workspace.package]` carrying one is read as carrying none.
|
|
22
|
+
*/
|
|
23
|
+
export function readInheritable(data: Record<string, unknown>): Partial<PackageInfo> {
|
|
24
|
+
const table = asRecord(data);
|
|
25
|
+
const found: Partial<PackageInfo> = {};
|
|
26
|
+
for (const key of ["version", "description", "license", "edition"] as const) {
|
|
27
|
+
const value = asString(table[key]);
|
|
28
|
+
if (value !== undefined) found[key] = value;
|
|
29
|
+
}
|
|
30
|
+
const authors = asList(table.authors);
|
|
31
|
+
return authors.length > 0 ? { ...found, authors } : found;
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Profile } from "../project.types.js";
|
|
2
|
+
import { asBoolean, asRecord } from "./scalars.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What each build is for, and what that costs.
|
|
6
|
+
*
|
|
7
|
+
* `dev` reports what it finds and carries on, because a project being worked on
|
|
8
|
+
* is half-written most of the time. `release` refuses to build over a problem.
|
|
9
|
+
* Neither has to be written in a manifest for the defaults to apply.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_PROFILES: Readonly<Record<string, Profile>> = {
|
|
12
|
+
dev: { strict: false },
|
|
13
|
+
release: { strict: true },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** `[profile.<name>]`, merged over {@link DEFAULT_PROFILES}. */
|
|
17
|
+
export function readProfiles(data: Record<string, unknown>): Record<string, Profile> {
|
|
18
|
+
const out: Record<string, Profile> = { ...DEFAULT_PROFILES };
|
|
19
|
+
for (const [name, table] of Object.entries(asRecord(data.profile))) {
|
|
20
|
+
out[name] = { ...out[name], ...readProfile(asRecord(table)) };
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readProfile(table: Record<string, unknown>): Profile {
|
|
26
|
+
const found: Profile = {};
|
|
27
|
+
for (const key of ["strict"] as const) {
|
|
28
|
+
const value = asBoolean(table[key]);
|
|
29
|
+
if (value !== undefined) found[key] = value;
|
|
30
|
+
}
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BuildTarget } from "../project.types.js";
|
|
2
|
+
import { asRecord, asRecords, asString } from "./scalars.js";
|
|
3
|
+
|
|
4
|
+
/** Where a `lib` target starts when the manifest does not say. */
|
|
5
|
+
export const LIB_ROOT = "src/lib.vn";
|
|
6
|
+
/** Where the default `bin` target starts when the manifest does not say. */
|
|
7
|
+
export const MAIN_ROOT = "src/main.vn";
|
|
8
|
+
/** Where additional `bin` targets are looked for by convention. */
|
|
9
|
+
export const BIN_DIR = "src/bin";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `[lib]` and `[[bin]]`: what this package builds.
|
|
13
|
+
*
|
|
14
|
+
* Only what the manifest *declares*. The conventional roots are filled in by
|
|
15
|
+
* whoever can look at the disk, because a convention is a claim about files
|
|
16
|
+
* existing and this reader is pure. A manifest declaring nothing is the common
|
|
17
|
+
* case, not an empty one.
|
|
18
|
+
*/
|
|
19
|
+
export function readTargets(data: Record<string, unknown>, packageName: string): BuildTarget[] {
|
|
20
|
+
const found: BuildTarget[] = [];
|
|
21
|
+
if (data.lib !== undefined) found.push(readLib(asRecord(data.lib), packageName));
|
|
22
|
+
for (const entry of asRecords(data.bin)) found.push(readBin(entry, packageName));
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readLib(table: Record<string, unknown>, packageName: string): BuildTarget {
|
|
27
|
+
return {
|
|
28
|
+
kind: "lib",
|
|
29
|
+
name: asString(table.name) ?? packageName,
|
|
30
|
+
path: asString(table.path) ?? LIB_ROOT,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readBin(table: Record<string, unknown>, packageName: string): BuildTarget {
|
|
35
|
+
const name = asString(table.name) ?? packageName;
|
|
36
|
+
return { kind: "bin", name, path: asString(table.path) ?? `${BIN_DIR}/${name}.vn` };
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PackageManagerName, ToolingSettings } from "../project.types.js";
|
|
2
|
+
import { asRecord, asString } from "./scalars.js";
|
|
3
|
+
|
|
4
|
+
const MANAGERS = new Set<string>(["pnpm", "npm", "bun", "yarn"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `[tooling]`: which package manager runs underneath.
|
|
8
|
+
*
|
|
9
|
+
* An unrecognised name falls back to pnpm rather than failing, so a manifest
|
|
10
|
+
* written against a newer toolchain still opens.
|
|
11
|
+
*/
|
|
12
|
+
export function readTooling(data: Record<string, unknown>): ToolingSettings {
|
|
13
|
+
const name = asString(asRecord(data.tooling).manager);
|
|
14
|
+
return { manager: name && MANAGERS.has(name) ? (name as PackageManagerName) : "pnpm" };
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { WorkspaceSettings } from "../project.types.js";
|
|
2
|
+
import { readDependencies } from "./read-dependencies.js";
|
|
3
|
+
import { readInheritable } from "./read-package.js";
|
|
4
|
+
import { asList, asRecord } from "./scalars.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `[workspace]`, or undefined when the manifest is a plain package.
|
|
8
|
+
*
|
|
9
|
+
* A root may also be a package, so `[workspace]` and `[package]` are read
|
|
10
|
+
* separately and never merged.
|
|
11
|
+
*/
|
|
12
|
+
export function readWorkspace(data: Record<string, unknown>): WorkspaceSettings | undefined {
|
|
13
|
+
if (data.workspace === undefined) return undefined;
|
|
14
|
+
const table = asRecord(data.workspace);
|
|
15
|
+
return {
|
|
16
|
+
members: asList(table.members),
|
|
17
|
+
exclude: asList(table.exclude),
|
|
18
|
+
defaultMembers: asList(table["default-members"]),
|
|
19
|
+
package: readInheritable(asRecord(table.package)),
|
|
20
|
+
dependencies: readDependencies(table.dependencies),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Reading a parsed TOML value as the shape the manifest expects. None of these
|
|
2
|
+
// throw: a malformed table reads as an empty one, so one bad line never stops a
|
|
3
|
+
// project from opening.
|
|
4
|
+
|
|
5
|
+
export function asRecord(value: unknown): Record<string, unknown> {
|
|
6
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function asStringMap(value: unknown): Record<string, string> {
|
|
10
|
+
const out: Record<string, string> = {};
|
|
11
|
+
for (const [key, item] of Object.entries(asRecord(value))) out[key] = String(item);
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function asList(value: unknown): readonly string[] {
|
|
16
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function asRecords(value: unknown): readonly Record<string, unknown>[] {
|
|
20
|
+
return Array.isArray(value) ? value.map(asRecord) : [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function asString(value: unknown): string | undefined {
|
|
24
|
+
return value === undefined || value === null ? undefined : String(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function asNumber(value: unknown): number | undefined {
|
|
28
|
+
const parsed = Number(value);
|
|
29
|
+
return value === undefined || Number.isNaN(parsed) ? undefined : parsed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function asBoolean(value: unknown): boolean | undefined {
|
|
33
|
+
if (value === undefined) return undefined;
|
|
34
|
+
return value === true || value === "true";
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { FormatSettings, Manifest } from "./manifest.types.js";
|
|
2
|
+
import { asBoolean, asList, asNumber, asRecord, asStringMap } from "./read/index.js";
|
|
3
|
+
|
|
4
|
+
type RunSettings = Pick<Manifest, "env" | "envFiles" | "paths" | "format">;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How the project runs, as against what it is: environments, path aliases and
|
|
8
|
+
* formatting.
|
|
9
|
+
*/
|
|
10
|
+
export function readRunSettings(data: Record<string, unknown>): RunSettings {
|
|
11
|
+
return {
|
|
12
|
+
env: readEnv(data.env),
|
|
13
|
+
envFiles: asList(asRecord(data.env).files),
|
|
14
|
+
paths: asStringMap(data.paths),
|
|
15
|
+
format: readFormat(data.format),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readFormat(value: unknown): FormatSettings {
|
|
20
|
+
const table = asRecord(value);
|
|
21
|
+
return {
|
|
22
|
+
indent: asNumber(table.indent),
|
|
23
|
+
tabs: asBoolean(table.tabs),
|
|
24
|
+
organize: asBoolean(table.organize),
|
|
25
|
+
sort: asBoolean(table.sort),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `files` configures where to read from; every other key names an environment. */
|
|
30
|
+
const RESERVED = new Set(["files"]);
|
|
31
|
+
|
|
32
|
+
function readEnv(value: unknown): Record<string, Record<string, string>> {
|
|
33
|
+
const out: Record<string, Record<string, string>> = {};
|
|
34
|
+
for (const [name, vars] of Object.entries(asRecord(value))) {
|
|
35
|
+
if (!RESERVED.has(name)) out[name] = asStringMap(vars);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Where a `#alias/…` specifier points. */
|
|
2
|
+
export interface AliasTarget {
|
|
3
|
+
/** The directory `[paths]` maps the alias to. */
|
|
4
|
+
dir: string;
|
|
5
|
+
/** What followed the alias, with the separating slash removed. */
|
|
6
|
+
rest: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Split a `#alias/rest` import specifier against `[paths]` from `venn.toml`.
|
|
11
|
+
*
|
|
12
|
+
* @returns undefined when the specifier names no configured alias, which the
|
|
13
|
+
* caller reads as a relative or bare path.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveAlias(args: {
|
|
16
|
+
spec: string;
|
|
17
|
+
paths: Record<string, string>;
|
|
18
|
+
}): AliasTarget | undefined {
|
|
19
|
+
const alias = Object.keys(args.paths).find((key) => args.spec.startsWith(`${key}/`));
|
|
20
|
+
if (!alias) return undefined;
|
|
21
|
+
return { dir: args.paths[alias] as string, rest: args.spec.slice(alias.length + 1) };
|
|
22
|
+
}
|