@telorun/analyzer 0.60.0 → 0.62.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/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +130 -9
- package/dist/artifact-layer-index.d.ts +13 -5
- package/dist/artifact-layer-index.d.ts.map +1 -1
- package/dist/artifact-layer-index.js +37 -13
- package/dist/artifact-selector.d.ts +10 -3
- package/dist/artifact-selector.d.ts.map +1 -1
- package/dist/artifact-selector.js +10 -1
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +97 -13
- package/dist/cel-bindings.d.ts +0 -6
- package/dist/cel-bindings.d.ts.map +1 -1
- package/dist/cel-bindings.js +3 -28
- package/dist/definition-registry.d.ts +17 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +31 -2
- package/dist/identifier-name.d.ts +114 -0
- package/dist/identifier-name.d.ts.map +1 -0
- package/dist/identifier-name.js +183 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/manifest-schemas.d.ts +81 -0
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +208 -6
- package/dist/module-file-claims.d.ts +7 -0
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +22 -2
- package/dist/module-library.d.ts +97 -0
- package/dist/module-library.d.ts.map +1 -0
- package/dist/module-library.js +162 -0
- package/dist/requires-block.d.ts +125 -0
- package/dist/requires-block.d.ts.map +1 -0
- package/dist/requires-block.js +182 -0
- package/dist/schema-keywords.d.ts +68 -0
- package/dist/schema-keywords.d.ts.map +1 -0
- package/dist/schema-keywords.js +324 -0
- package/dist/schema-region.d.ts +12 -1
- package/dist/schema-region.d.ts.map +1 -1
- package/dist/schema-region.js +12 -1
- package/dist/telo-version.d.ts +3 -0
- package/dist/telo-version.d.ts.map +1 -0
- package/dist/telo-version.js +8 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-identifier-names.d.ts +31 -0
- package/dist/validate-identifier-names.d.ts.map +1 -0
- package/dist/validate-identifier-names.js +144 -0
- package/dist/validate-module-artifact.d.ts.map +1 -1
- package/dist/validate-module-artifact.js +53 -1
- package/dist/validate-observed-state.d.ts +9 -2
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +9 -2
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +5 -26
- package/dist/validate-requires.d.ts +49 -0
- package/dist/validate-requires.d.ts.map +1 -0
- package/dist/validate-requires.js +99 -0
- package/dist/value-type-keyword.d.ts +1 -1
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +1 -0
- package/dist/version-range.d.ts +88 -0
- package/dist/version-range.d.ts.map +1 -0
- package/dist/version-range.js +173 -0
- package/package.json +2 -2
- package/src/analyzer.ts +146 -10
- package/src/artifact-layer-index.ts +47 -14
- package/src/artifact-selector.ts +15 -4
- package/src/builtins.ts +102 -13
- package/src/cel-bindings.ts +3 -28
- package/src/definition-registry.ts +30 -2
- package/src/identifier-name.ts +228 -0
- package/src/index.ts +44 -1
- package/src/manifest-schemas.ts +223 -4
- package/src/module-file-claims.ts +32 -2
- package/src/module-library.ts +208 -0
- package/src/requires-block.ts +253 -0
- package/src/schema-keywords.ts +359 -0
- package/src/schema-region.ts +12 -1
- package/src/telo-version.ts +9 -0
- package/src/types.ts +32 -0
- package/src/validate-identifier-names.ts +173 -0
- package/src/validate-module-artifact.ts +56 -0
- package/src/validate-observed-state.ts +9 -2
- package/src/validate-references.ts +5 -26
- package/src/validate-requires.ts +129 -0
- package/src/value-type-keyword.ts +1 -0
- package/src/version-range.ts +238 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A module's **exported code** — the `exports.code:` block on a `Telo.Library`
|
|
3
|
+
* doc, which names the entry point a *sibling module's* controller bundle
|
|
4
|
+
* resolves this module's bare specifier to.
|
|
5
|
+
*
|
|
6
|
+
* ```yaml
|
|
7
|
+
* exports:
|
|
8
|
+
* kinds:
|
|
9
|
+
* - Store
|
|
10
|
+
* code:
|
|
11
|
+
* - specifier: "@telorun/kv-store"
|
|
12
|
+
* format: js
|
|
13
|
+
* path: ./nodejs/kv-store.mjs
|
|
14
|
+
* source: ./nodejs/src/index.ts
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ## Why it sits under `exports:`
|
|
18
|
+
*
|
|
19
|
+
* A library already declares what crosses its boundary — the kinds importers may
|
|
20
|
+
* name, the resource instances they may `!ref`. This is the same statement about
|
|
21
|
+
* its *code*, and it gates the same way: a specifier nobody declares resolves to
|
|
22
|
+
* nothing. Putting it beside them keeps one block for "reachable from outside"
|
|
23
|
+
* rather than a second top-level key whose name (`library:` on a `Telo.Library`)
|
|
24
|
+
* meant a different thing from the kind one line above it.
|
|
25
|
+
*
|
|
26
|
+
* ## Why it is not a package URL
|
|
27
|
+
*
|
|
28
|
+
* `controllers:` names a PURL because it must be able to say `pkg:npm/…` or
|
|
29
|
+
* `pkg:cargo/…` — an ecosystem fetch. This entry never fetches: it names a file
|
|
30
|
+
* the module already ships, so `pkg:telo/local/` would be three constant segments
|
|
31
|
+
* before the first real datum. What is left after removing them is exactly these
|
|
32
|
+
* fields, and as data they are visually editable, where a query string is one
|
|
33
|
+
* opaque text box.
|
|
34
|
+
*
|
|
35
|
+
* The **model** is unchanged: `format` plus the optional platform axes build the
|
|
36
|
+
* same `ArtifactSelector` a controller candidate does, so layer matching, platform
|
|
37
|
+
* fallthrough and lazy materialization are inherited whole.
|
|
38
|
+
*
|
|
39
|
+
* ## Why the specifier is declared here
|
|
40
|
+
*
|
|
41
|
+
* A bundle imports the bare specifier `@telorun/sql`; the consumer's manifest
|
|
42
|
+
* declares the dependency as `Sql: ../sql`. Something has to connect the two, and
|
|
43
|
+
* it is the *library* that says so, once, rather than each of its consumers:
|
|
44
|
+
*
|
|
45
|
+
* - the specifier is a property of the library — its name in a host language's
|
|
46
|
+
* ecosystem — not of the relationship, so N consumers cannot disagree about it
|
|
47
|
+
* and adding a consumer restates nothing;
|
|
48
|
+
* - it sits beside the format, which keeps runtime **derived, never declared**:
|
|
49
|
+
* the entry says `format: js`, and a Rust entry carries `specifier:
|
|
50
|
+
* telorun-sql` with no runtime-keyed map anywhere.
|
|
51
|
+
*
|
|
52
|
+
* **One specifier, one entry point.** Subpaths are deliberately not
|
|
53
|
+
* representable: reproducing npm's `exports` map inside the artifact would pull a
|
|
54
|
+
* package manager's resolution semantics into Telo, which is what the "only
|
|
55
|
+
* workspace modules are de-inlined" rule refuses on `kysely`'s behalf.
|
|
56
|
+
*
|
|
57
|
+
* `Telo.Application` has no `exports:` block at all — an application is a root
|
|
58
|
+
* with no importer, so nothing could resolve a specifier to it.
|
|
59
|
+
*
|
|
60
|
+
* Browser-safe: string work only. Whether the named file EXISTS is a separate
|
|
61
|
+
* question, asked by the Node-side caller that has a directory.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import {
|
|
65
|
+
ArtifactSelectorError,
|
|
66
|
+
PLATFORM_AXES,
|
|
67
|
+
selectorFromQualifiers,
|
|
68
|
+
type ArtifactSelector,
|
|
69
|
+
} from "./artifact-selector.js";
|
|
70
|
+
|
|
71
|
+
/** Every key an entry may carry: the two locators, plus the selector axes. */
|
|
72
|
+
const KNOWN_KEYS = new Set<string>(["specifier", "path", "source", "format", ...PLATFORM_AXES]);
|
|
73
|
+
|
|
74
|
+
export interface LibraryCandidate {
|
|
75
|
+
/** The bare specifier a sibling's controller bundle imports this library by. */
|
|
76
|
+
readonly specifier: string;
|
|
77
|
+
/** Module-root-relative path of the built entry point. */
|
|
78
|
+
readonly path: string;
|
|
79
|
+
/** Module-root-relative TypeScript source it is built from (`source:`), when
|
|
80
|
+
* the entry names one. Present only while the module is a working copy; a
|
|
81
|
+
* published artifact ships no `src/`. */
|
|
82
|
+
readonly localPath?: string;
|
|
83
|
+
readonly selector: ArtifactSelector;
|
|
84
|
+
/** Where the entry was written, for diagnostics. */
|
|
85
|
+
readonly origin: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Why an `exports.code` entry could not be read. Returned rather than thrown so
|
|
89
|
+
* the analyzer can report every entry of a block, and so a reader on the load
|
|
90
|
+
* path can carry on with the entries that are well-formed. */
|
|
91
|
+
export interface LibraryCandidateProblem {
|
|
92
|
+
readonly origin: string;
|
|
93
|
+
readonly detail: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface LibraryCandidates {
|
|
97
|
+
readonly candidates: LibraryCandidate[];
|
|
98
|
+
readonly problems: LibraryCandidateProblem[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Normalize a `path` / `source` value to the manifest-relative POSIX form the
|
|
102
|
+
* file selector returns, so membership is a string comparison. */
|
|
103
|
+
function normalizeRelative(value: string): string {
|
|
104
|
+
return value.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function requiredString(
|
|
108
|
+
entry: Record<string, unknown>,
|
|
109
|
+
key: string,
|
|
110
|
+
): { value: string } | { detail: string } {
|
|
111
|
+
const raw = entry[key];
|
|
112
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
113
|
+
return { detail: `'${key}' is required and must be a non-empty string.` };
|
|
114
|
+
}
|
|
115
|
+
return { value: raw.trim() };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read the `exports.code:` block off an owner document's JSON projection.
|
|
120
|
+
*
|
|
121
|
+
* Everything malformed is a problem rather than a silent skip: an entry that
|
|
122
|
+
* cannot be read names no entry point, so a consumer's bundle falls back to
|
|
123
|
+
* *inlining* the library — the duplicated module scope this whole mechanism
|
|
124
|
+
* exists to remove — and it does so on someone else's machine.
|
|
125
|
+
*/
|
|
126
|
+
export function readLibraryCandidates(ownerJson: unknown): LibraryCandidates {
|
|
127
|
+
const declared = (ownerJson as { exports?: { code?: unknown } } | null)?.exports?.code;
|
|
128
|
+
const candidates: LibraryCandidate[] = [];
|
|
129
|
+
const problems: LibraryCandidateProblem[] = [];
|
|
130
|
+
if (declared === undefined) return { candidates, problems };
|
|
131
|
+
if (!Array.isArray(declared)) {
|
|
132
|
+
return {
|
|
133
|
+
candidates,
|
|
134
|
+
problems: [{ origin: "exports.code", detail: "expected a list of entries." }],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
declared.forEach((raw, index) => {
|
|
139
|
+
const origin = `exports.code[${index}]`;
|
|
140
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
141
|
+
problems.push({ origin, detail: "expected an object." });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const entry = raw as Record<string, unknown>;
|
|
145
|
+
|
|
146
|
+
const unknown = Object.keys(entry).filter((key) => !KNOWN_KEYS.has(key));
|
|
147
|
+
if (unknown.length > 0) {
|
|
148
|
+
// Reported, never ignored: an unrecognized platform axis would leave the
|
|
149
|
+
// entry platform-neutral and offer a single-platform file to every host.
|
|
150
|
+
problems.push({
|
|
151
|
+
origin,
|
|
152
|
+
detail:
|
|
153
|
+
`unknown ${unknown.length === 1 ? "key" : "keys"} ${unknown.map((k) => `'${k}'`).join(", ")}. ` +
|
|
154
|
+
`Known: ${[...KNOWN_KEYS].join(", ")}.`,
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const specifier = requiredString(entry, "specifier");
|
|
160
|
+
if ("detail" in specifier) {
|
|
161
|
+
problems.push({ origin, detail: specifier.detail });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const file = requiredString(entry, "path");
|
|
165
|
+
if ("detail" in file) {
|
|
166
|
+
problems.push({ origin: `${origin} ('${specifier.value}')`, detail: file.detail });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Explicit rather than inferred from the file extension: a `.mjs` can be
|
|
170
|
+
// wasm glue, and an inference rule is something every other runtime's reader
|
|
171
|
+
// would have to copy exactly.
|
|
172
|
+
const format = requiredString(entry, "format");
|
|
173
|
+
if ("detail" in format) {
|
|
174
|
+
problems.push({ origin: `${origin} ('${specifier.value}')`, detail: format.detail });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let selector: ArtifactSelector;
|
|
179
|
+
try {
|
|
180
|
+
selector = selectorFromQualifiers(format.value, entry, `${origin} ('${specifier.value}')`);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
problems.push({
|
|
183
|
+
origin: `${origin} ('${specifier.value}')`,
|
|
184
|
+
detail: err instanceof ArtifactSelectorError ? err.message : String(err),
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const source = entry.source;
|
|
190
|
+
if (source !== undefined && (typeof source !== "string" || source.trim() === "")) {
|
|
191
|
+
problems.push({
|
|
192
|
+
origin: `${origin} ('${specifier.value}')`,
|
|
193
|
+
detail: "'source' must be a non-empty string when present.",
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
candidates.push({
|
|
199
|
+
specifier: specifier.value,
|
|
200
|
+
path: normalizeRelative(file.value),
|
|
201
|
+
...(typeof source === "string" ? { localPath: normalizeRelative(source.trim()) } : {}),
|
|
202
|
+
selector,
|
|
203
|
+
origin: `${origin} ('${specifier.value}')`,
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return { candidates, problems };
|
|
208
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single reader of a module doc's `requires:` block — the version ranges of
|
|
3
|
+
* runtime a module declares itself verified against. The load gate, the CLI's
|
|
4
|
+
* publish preflight and `upgrade`'s candidate filter all recognise the block
|
|
5
|
+
* here and nowhere else, the one-accessor rule `ref-slot.ts` and `zone-slot.ts`
|
|
6
|
+
* established. Browser-safe: no Node built-ins, so the editor reaches the
|
|
7
|
+
* identical rule the kernel does.
|
|
8
|
+
*
|
|
9
|
+
* ```yaml
|
|
10
|
+
* requires:
|
|
11
|
+
* telo: ">=0.80.0"
|
|
12
|
+
* host:
|
|
13
|
+
* node: ">=20.0.0"
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* **Two tiers, and the split is not cosmetic.** `telo` names the *manifest
|
|
17
|
+
* surface generation* a runtime implements — one scale shared by every kernel,
|
|
18
|
+
* Node, Rust or Go, independent of each kernel's own release identity — and it
|
|
19
|
+
* is the one axis verified by EXECUTION, by running the CLI at each edge of the
|
|
20
|
+
* declared range. Host axes cannot be edge-verified by any CI; they are asserted
|
|
21
|
+
* by the author and compared against the version the running host reports
|
|
22
|
+
* ({@link HostVersions}). A flat map would imply one semantics for both.
|
|
23
|
+
*
|
|
24
|
+
* Nesting is also what disambiguates the names: `nodejs` and `rust` are already
|
|
25
|
+
* *kernel labels* in this repo (`LABEL_TO_PURL_TYPE`, an `imports:` entry's
|
|
26
|
+
* `runtime:`), so a top-level `node:` reads as the Node kernel rather than the
|
|
27
|
+
* Node.js runtime — and no word escapes that, because the host runtime and the
|
|
28
|
+
* kernel implementation genuinely share a name. Under `host:` position carries
|
|
29
|
+
* the disambiguation and no word has to.
|
|
30
|
+
*
|
|
31
|
+
* **Ordering is normative: `telo` is checked before `host`, and before any
|
|
32
|
+
* unknown-axis complaint.** A module using an axis introduced in telo 0.85 also
|
|
33
|
+
* declares telo `>=0.85`, so an older runtime fails on the telo axis first and
|
|
34
|
+
* never has to decide what an axis it has never heard of means. That is what
|
|
35
|
+
* makes the block safely extensible; consumers get the order from
|
|
36
|
+
* {@link evaluateRequires} rather than re-deriving it.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { parseModuleVersion } from "./module-version-order.js";
|
|
40
|
+
import {
|
|
41
|
+
isUnsatisfiable,
|
|
42
|
+
parseVersionRange,
|
|
43
|
+
rangeAccepts,
|
|
44
|
+
type VersionRange,
|
|
45
|
+
} from "./version-range.js";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Host axes this analyzer knows.
|
|
49
|
+
*
|
|
50
|
+
* **An axis is in this list only when something checks it.** A declared
|
|
51
|
+
* requirement nothing compares is worse than no requirement at all: it validates,
|
|
52
|
+
* it reads as protection, and it silently protects nobody — the exact failure
|
|
53
|
+
* class this whole mechanism exists to remove, reintroduced inside it. So `rustc`
|
|
54
|
+
* is deliberately absent until the slice that builds controller crates can
|
|
55
|
+
* compare it; adding it there is a one-line change here plus a supplier in
|
|
56
|
+
* {@link HostVersions}, and until then an author writing it is told it is not a
|
|
57
|
+
* known axis rather than quietly reassured.
|
|
58
|
+
*
|
|
59
|
+
* Extending the set is a telo release, which is exactly why a module using a new
|
|
60
|
+
* axis must also raise its `telo` bound — and why `telo` is checked first.
|
|
61
|
+
*/
|
|
62
|
+
export const KNOWN_HOST_AXES = ["node"] as const;
|
|
63
|
+
export type HostAxis = (typeof KNOWN_HOST_AXES)[number];
|
|
64
|
+
|
|
65
|
+
/** Top-level keys of the block. `host` is a container, `telo` a range. */
|
|
66
|
+
const KNOWN_AXES = ["telo", "host"] as const;
|
|
67
|
+
|
|
68
|
+
export interface RequiresBlock {
|
|
69
|
+
/** The surface generation range, when declared. */
|
|
70
|
+
telo?: VersionRange;
|
|
71
|
+
/** Declared host axes, keyed by axis name. Empty when `host:` is absent. */
|
|
72
|
+
host: Partial<Record<HostAxis, VersionRange>>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RequiresIssue {
|
|
76
|
+
/** Dotted path within the doc, e.g. `requires.telo` or `requires.host.node`. */
|
|
77
|
+
path: string;
|
|
78
|
+
message: string;
|
|
79
|
+
/** The spelling to use instead, when there is an unambiguous one. */
|
|
80
|
+
hint?: string;
|
|
81
|
+
/** True for an unrecognized axis, which consumers suppress while the `telo`
|
|
82
|
+
* requirement itself is unmet — an older runtime not knowing a newer axis is
|
|
83
|
+
* a consequence of the version skew, not a second defect to report. */
|
|
84
|
+
unknownAxis?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ReadRequiresResult {
|
|
88
|
+
/** Present whenever the doc carries a `requires:` key at all, even a malformed
|
|
89
|
+
* one — so a consumer can tell "declared nothing" from "declared badly". */
|
|
90
|
+
declared: boolean;
|
|
91
|
+
block: RequiresBlock;
|
|
92
|
+
issues: RequiresIssue[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const EMPTY: RequiresBlock = { host: {} };
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read and parse the block off a module doc. Never throws. A malformed entry
|
|
99
|
+
* yields an issue AND is omitted from the block, so a consumer enforcing the
|
|
100
|
+
* block never silently treats garbage as a satisfied requirement — the issue is
|
|
101
|
+
* what makes the manifest fail, exactly as a malformed zone annotation does.
|
|
102
|
+
*/
|
|
103
|
+
export function readRequires(doc: Record<string, unknown> | undefined): ReadRequiresResult {
|
|
104
|
+
const raw = doc?.requires;
|
|
105
|
+
if (raw === undefined) return { declared: false, block: EMPTY, issues: [] };
|
|
106
|
+
|
|
107
|
+
const issues: RequiresIssue[] = [];
|
|
108
|
+
const block: RequiresBlock = { host: {} };
|
|
109
|
+
|
|
110
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
111
|
+
issues.push({
|
|
112
|
+
path: "requires",
|
|
113
|
+
message: `'requires' must be a mapping of axes, got ${describe(raw)}.`,
|
|
114
|
+
});
|
|
115
|
+
return { declared: true, block, issues };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const entries = raw as Record<string, unknown>;
|
|
119
|
+
|
|
120
|
+
for (const key of Object.keys(entries)) {
|
|
121
|
+
if (!(KNOWN_AXES as readonly string[]).includes(key)) {
|
|
122
|
+
issues.push({
|
|
123
|
+
path: `requires.${key}`,
|
|
124
|
+
message:
|
|
125
|
+
`'requires.${key}' is not a known axis. This runtime knows ` +
|
|
126
|
+
`${KNOWN_AXES.map((a) => `'${a}'`).join(" and ")}; host requirements go under 'host'.`,
|
|
127
|
+
unknownAxis: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (entries.telo !== undefined) {
|
|
133
|
+
const range = parseRangeAt(entries.telo, "requires.telo", issues);
|
|
134
|
+
if (range) block.telo = range;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (entries.host !== undefined) {
|
|
138
|
+
const host = entries.host;
|
|
139
|
+
if (host === null || typeof host !== "object" || Array.isArray(host)) {
|
|
140
|
+
issues.push({
|
|
141
|
+
path: "requires.host",
|
|
142
|
+
message: `'requires.host' must be a mapping of host axes, got ${describe(host)}.`,
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
for (const [axis, value] of Object.entries(host as Record<string, unknown>)) {
|
|
146
|
+
if (!(KNOWN_HOST_AXES as readonly string[]).includes(axis)) {
|
|
147
|
+
issues.push({
|
|
148
|
+
path: `requires.host.${axis}`,
|
|
149
|
+
message:
|
|
150
|
+
`'requires.host.${axis}' is not a known host axis. This runtime knows ` +
|
|
151
|
+
`${KNOWN_HOST_AXES.map((a) => `'${a}'`).join(", ")}.`,
|
|
152
|
+
unknownAxis: true,
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const range = parseRangeAt(value, `requires.host.${axis}`, issues);
|
|
157
|
+
if (range) block.host[axis as HostAxis] = range;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { declared: true, block, issues };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseRangeAt(
|
|
166
|
+
value: unknown,
|
|
167
|
+
path: string,
|
|
168
|
+
issues: RequiresIssue[],
|
|
169
|
+
): VersionRange | undefined {
|
|
170
|
+
const result = parseVersionRange(value);
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
issues.push({ path, message: `'${path}': ${result.error.message}.`, hint: result.error.hint });
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
if (isUnsatisfiable(result.range)) {
|
|
176
|
+
issues.push({
|
|
177
|
+
path,
|
|
178
|
+
message: `'${path}': '${result.range.raw}' admits no version — its bounds exclude each other.`,
|
|
179
|
+
});
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
return result.range;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** What a runtime concluded about a module's declared requirements. */
|
|
186
|
+
export type RequiresVerdict =
|
|
187
|
+
| { satisfied: true }
|
|
188
|
+
/** An axis whose declared range excludes the version this runtime reported. */
|
|
189
|
+
| {
|
|
190
|
+
satisfied: false;
|
|
191
|
+
axis: "telo" | HostAxis;
|
|
192
|
+
declared: VersionRange;
|
|
193
|
+
running: string;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** The versions a host can speak for. Absent entries are not checked — the
|
|
197
|
+
* editor has no host to report, and an axis nothing supplies is skipped rather
|
|
198
|
+
* than guessed. Every axis in {@link KNOWN_HOST_AXES} has a supplier; an axis
|
|
199
|
+
* with none does not belong in the vocabulary (see the note there). */
|
|
200
|
+
export interface HostVersions {
|
|
201
|
+
node?: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Evaluate a module's declared requirements against the runtime performing the
|
|
206
|
+
* analysis.
|
|
207
|
+
*
|
|
208
|
+
* `telo` is checked FIRST and short-circuits: a module using a host axis
|
|
209
|
+
* introduced in a later telo also declares that telo, so an older runtime must
|
|
210
|
+
* report the version skew rather than a host axis it may not even know. Absent
|
|
211
|
+
* declarations are satisfied — the bootstrap rule, permanent for everything
|
|
212
|
+
* published before the mechanism existed.
|
|
213
|
+
*
|
|
214
|
+
* **A version this cannot PARSE is satisfied, on every axis.** A runtime that
|
|
215
|
+
* cannot name its own version must not start rejecting modules on the strength
|
|
216
|
+
* of a number it could not read — the refusal-to-guess `module-version-order.ts`
|
|
217
|
+
* makes, pointed in the safe direction. The test is a parse, not a shape: `0.76`
|
|
218
|
+
* and `2024.1` look like versions and are not three-part ones, so a cheaper
|
|
219
|
+
* check (a leading digit, say) would fail them CLOSED and gate every module in
|
|
220
|
+
* the graph on a number nothing could compare. `AnalysisOptions.teloVersion` is
|
|
221
|
+
* hand-written by definition, so that is exactly where such a value arrives.
|
|
222
|
+
*/
|
|
223
|
+
export function evaluateRequires(
|
|
224
|
+
block: RequiresBlock,
|
|
225
|
+
running: string | undefined,
|
|
226
|
+
host: HostVersions = {},
|
|
227
|
+
): RequiresVerdict {
|
|
228
|
+
const telo = check("telo", block.telo, running);
|
|
229
|
+
if (telo) return telo;
|
|
230
|
+
for (const axis of KNOWN_HOST_AXES) {
|
|
231
|
+
const verdict = check(axis, block.host[axis], host[axis]);
|
|
232
|
+
if (verdict) return verdict;
|
|
233
|
+
}
|
|
234
|
+
return { satisfied: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** One axis, or `undefined` when it is satisfied / undeclared / unreportable. */
|
|
238
|
+
function check(
|
|
239
|
+
axis: "telo" | HostAxis,
|
|
240
|
+
declared: VersionRange | undefined,
|
|
241
|
+
running: string | undefined,
|
|
242
|
+
): RequiresVerdict | undefined {
|
|
243
|
+
if (!declared || !running) return undefined;
|
|
244
|
+
if (!parseModuleVersion(running)) return undefined;
|
|
245
|
+
if (rangeAccepts(declared, running)) return undefined;
|
|
246
|
+
return { satisfied: false, axis, declared, running };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function describe(value: unknown): string {
|
|
250
|
+
if (value === null) return "null";
|
|
251
|
+
if (Array.isArray(value)) return "an array";
|
|
252
|
+
return typeof value;
|
|
253
|
+
}
|