@aventara/client 0.0.0-stage → 0.1.0-pilot.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 +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +268 -2
- package/dist/avclient.bin.d.ts +2 -0
- package/dist/avclient.bin.js +15 -0
- package/dist/cli/command.parser.d.ts +30 -0
- package/dist/cli/command.parser.js +132 -0
- package/dist/cli/generate.command.d.ts +24 -0
- package/dist/cli/generate.command.js +41 -0
- package/dist/cli/generation-failure.renderer.d.ts +6 -0
- package/dist/cli/generation-failure.renderer.js +54 -0
- package/dist/cli/generation-success.renderer.d.ts +32 -0
- package/dist/cli/generation-success.renderer.js +47 -0
- package/dist/cli/terminal.prompter.d.ts +13 -0
- package/dist/cli/terminal.prompter.js +53 -0
- package/dist/cli/warning.renderer.d.ts +10 -0
- package/dist/cli/warning.renderer.js +14 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +71 -0
- package/dist/config/client-config.interface.d.ts +62 -0
- package/dist/config/client-config.interface.js +14 -0
- package/dist/config/config.loader.d.ts +33 -0
- package/dist/config/config.loader.js +80 -0
- package/dist/config/config.resolver.d.ts +50 -0
- package/dist/config/config.resolver.js +126 -0
- package/dist/config/env.cascade.d.ts +84 -0
- package/dist/config/env.cascade.js +126 -0
- package/dist/contract/contract.acceptance.d.ts +77 -0
- package/dist/contract/contract.acceptance.js +124 -0
- package/dist/contract/contract.fetcher.d.ts +64 -0
- package/dist/contract/contract.fetcher.js +85 -0
- package/dist/contract/contract.loader.d.ts +32 -0
- package/dist/contract/contract.loader.js +32 -0
- package/dist/emit/banner.emitter.d.ts +31 -0
- package/dist/emit/banner.emitter.js +42 -0
- package/dist/emit/client-surface.emitter.d.ts +32 -0
- package/dist/emit/client-surface.emitter.js +236 -0
- package/dist/emit/client-tree.emitter.d.ts +37 -0
- package/dist/emit/client-tree.emitter.js +103 -0
- package/dist/emit/contract-carrier.emitter.d.ts +13 -0
- package/dist/emit/contract-carrier.emitter.js +60 -0
- package/dist/emit/derivation.emitter.d.ts +45 -0
- package/dist/emit/derivation.emitter.js +233 -0
- package/dist/emit/descriptor.emitter.d.ts +4 -0
- package/dist/emit/descriptor.emitter.js +97 -0
- package/dist/emit/emitted-tree.interface.d.ts +61 -0
- package/dist/emit/emitted-tree.interface.js +18 -0
- package/dist/emit/enum.emitter.d.ts +24 -0
- package/dist/emit/enum.emitter.js +42 -0
- package/dist/emit/name.deriver.d.ts +153 -0
- package/dist/emit/name.deriver.js +411 -0
- package/dist/emit/named-type.emitter.d.ts +32 -0
- package/dist/emit/named-type.emitter.js +50 -0
- package/dist/emit/runtime.emitter.d.ts +87 -0
- package/dist/emit/runtime.emitter.js +707 -0
- package/dist/emit/scalar.codec.d.ts +63 -0
- package/dist/emit/scalar.codec.js +498 -0
- package/dist/emit/transaction.emitter.d.ts +17 -0
- package/dist/emit/transaction.emitter.js +438 -0
- package/dist/generate.d.ts +123 -0
- package/dist/generate.js +98 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/init/client-config.template.d.ts +6 -0
- package/dist/init/client-config.template.js +22 -0
- package/dist/init/client-init.errors.d.ts +9 -0
- package/dist/init/client-init.errors.js +9 -0
- package/dist/init/client-init.orchestrator.d.ts +3 -0
- package/dist/init/client-init.orchestrator.js +82 -0
- package/dist/init/client-init.planner.d.ts +26 -0
- package/dist/init/client-init.planner.js +88 -0
- package/dist/init/client-init.questions.d.ts +52 -0
- package/dist/init/client-init.questions.js +124 -0
- package/dist/init/client-project.inspector.d.ts +15 -0
- package/dist/init/client-project.inspector.js +32 -0
- package/dist/init/command.runner.d.ts +8 -0
- package/dist/init/command.runner.js +17 -0
- package/dist/node-version.guard.d.ts +8 -0
- package/dist/node-version.guard.js +59 -0
- package/dist/output/output.validator.d.ts +76 -0
- package/dist/output/output.validator.js +254 -0
- package/dist/output/output.writer.d.ts +162 -0
- package/dist/output/output.writer.js +499 -0
- package/package.json +47 -3
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { canonicalizeContract } from "@aventara/core";
|
|
2
|
+
import { acceptClientContract } from "../contract/contract.acceptance.js";
|
|
3
|
+
import { withGeneratedBanner } from "./banner.emitter.js";
|
|
4
|
+
/**
|
|
5
|
+
* The carrier (Q4 = A): `generated/contract.ts` holds the ClientContract the
|
|
6
|
+
* client was generated against, as its served canonical bytes (RFC 8785, §19.2)
|
|
7
|
+
* verbatim — which are a valid TypeScript type literal (plan M4):
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* export type ClientContractShape = {"enums":{},…};
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* One artefact, three readers: the derivation's `C` (S4); the generator on a
|
|
14
|
+
* `304` (Q6, S7), which parses it back with {@link parseContractCarrier}; and a
|
|
15
|
+
* person auditing what the client was generated against. It costs no runtime
|
|
16
|
+
* byte — nothing imports it as a value.
|
|
17
|
+
*
|
|
18
|
+
* The bytes are `canonicalizeContract`'s over the ACCEPTED contract: the body a
|
|
19
|
+
* deployment serves is those bytes (`_contract` serves `canonicalizeContract`'s
|
|
20
|
+
* output, and acceptance verified the hash over the same canonical form), and a
|
|
21
|
+
* contract that arrives in another key order still emits them (C-843).
|
|
22
|
+
*/
|
|
23
|
+
const CARRIER_PREFIX = "export type ClientContractShape = ";
|
|
24
|
+
const CARRIER_SUFFIX = ";\n";
|
|
25
|
+
/** `generated/contract.ts`, before the banner. */
|
|
26
|
+
export function emitContractCarrierModule(contract) {
|
|
27
|
+
return {
|
|
28
|
+
path: "contract.ts",
|
|
29
|
+
source: `${CARRIER_PREFIX}${canonicalizeContract(contract)}${CARRIER_SUFFIX}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The ClientContract a carrier file holds — its whole text, banner included —
|
|
34
|
+
* or `undefined` when it is not one this generator wrote over a contract that
|
|
35
|
+
* still verifies: a foreign prefix or suffix, bytes that are not JSON, a body
|
|
36
|
+
* that is not a ClientContract or whose hash does not re-verify, or bytes that
|
|
37
|
+
* are not that contract's canonical form. Never trusted otherwise (Q6: a carrier
|
|
38
|
+
* that does not re-hash is treated as absent).
|
|
39
|
+
*/
|
|
40
|
+
export async function parseContractCarrier(text) {
|
|
41
|
+
const opening = withGeneratedBanner(CARRIER_PREFIX);
|
|
42
|
+
if (!text.startsWith(opening) || !text.endsWith(CARRIER_SUFFIX)) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const bytes = text.slice(opening.length, text.length - CARRIER_SUFFIX.length);
|
|
46
|
+
let body;
|
|
47
|
+
try {
|
|
48
|
+
body = JSON.parse(bytes);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const acceptance = await acceptClientContract(body);
|
|
54
|
+
if (!acceptance.accepted) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return canonicalizeContract(acceptance.contract) === bytes
|
|
58
|
+
? acceptance.contract
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { EmittedModule } from "./emitted-tree.interface.js";
|
|
2
|
+
/**
|
|
3
|
+
* The derivation, transported (Phase 12-rest S2, Q1 = A). A generated client's
|
|
4
|
+
* argument and result types are core's own `OperationArgumentsFor`,
|
|
5
|
+
* `OperationResultFor` and the call grammar (`OperationCall`, …) — never a second
|
|
6
|
+
* spelling. They reach the emitted tree as core's PUBLISHED DECLARATIONS, copied
|
|
7
|
+
* under `generated/derivation/` at their path relative to core's `dist`:
|
|
8
|
+
*
|
|
9
|
+
* - declarations, not sources: the sources' closure carries value imports (the
|
|
10
|
+
* canonicaliser, the wire grammars), the declarations' carries none (plan M1);
|
|
11
|
+
* a `.d.ts` cannot carry runtime, so a bundler never sees one;
|
|
12
|
+
* - under their own directory, so core's `runtime/decimal.d.ts` cannot shadow the
|
|
13
|
+
* emitted `generated/runtime/decimal.ts`;
|
|
14
|
+
* - byte for byte, except the trailing `//# sourceMappingURL=…` line, which names
|
|
15
|
+
* a map the tree does not hold.
|
|
16
|
+
*
|
|
17
|
+
* The closure is walked here, at generation time, from {@link DERIVATION_ROOTS}
|
|
18
|
+
* over whichever `@aventara/core` this package resolves — so the output is a
|
|
19
|
+
* function of the core version the generator depends on, as the rest of it is a
|
|
20
|
+
* function of the generator. A closure that names a package, carries a
|
|
21
|
+
* triple-slash reference, or leaves core's declarations is refused: it cannot be
|
|
22
|
+
* transported, and core's own gate (`emittable-closure.gate.spec.ts`) exists so
|
|
23
|
+
* that it never is.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The modules whose declarations the generated client copies — the roots of core's
|
|
27
|
+
* `emittable-closure.gate.spec.ts`, mirrored (core cannot export a test constant,
|
|
28
|
+
* and this package cannot read core's tests).
|
|
29
|
+
*/
|
|
30
|
+
export declare const DERIVATION_ROOTS: readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* Reads one declaration file by its path relative to core's `dist`
|
|
33
|
+
* (POSIX-separated); `undefined` when there is none.
|
|
34
|
+
*/
|
|
35
|
+
export type DeclarationReader = (relative: string) => string | undefined;
|
|
36
|
+
/** The published declarations of the `@aventara/core` this package resolves. */
|
|
37
|
+
export declare function publishedCoreDeclarations(): DeclarationReader;
|
|
38
|
+
/**
|
|
39
|
+
* Core's declaration closure from {@link DERIVATION_ROOTS}, as modules under
|
|
40
|
+
* `derivation/`, in UTF-16 code-unit order of their path.
|
|
41
|
+
*
|
|
42
|
+
* @throws Error when the closure cannot be transported — a defect of the
|
|
43
|
+
* installed `@aventara/core`, never of the consumer's input.
|
|
44
|
+
*/
|
|
45
|
+
export declare function emitDerivationModules(read?: DeclarationReader): readonly EmittedModule[];
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* The derivation, transported (Phase 12-rest S2, Q1 = A). A generated client's
|
|
6
|
+
* argument and result types are core's own `OperationArgumentsFor`,
|
|
7
|
+
* `OperationResultFor` and the call grammar (`OperationCall`, …) — never a second
|
|
8
|
+
* spelling. They reach the emitted tree as core's PUBLISHED DECLARATIONS, copied
|
|
9
|
+
* under `generated/derivation/` at their path relative to core's `dist`:
|
|
10
|
+
*
|
|
11
|
+
* - declarations, not sources: the sources' closure carries value imports (the
|
|
12
|
+
* canonicaliser, the wire grammars), the declarations' carries none (plan M1);
|
|
13
|
+
* a `.d.ts` cannot carry runtime, so a bundler never sees one;
|
|
14
|
+
* - under their own directory, so core's `runtime/decimal.d.ts` cannot shadow the
|
|
15
|
+
* emitted `generated/runtime/decimal.ts`;
|
|
16
|
+
* - byte for byte, except the trailing `//# sourceMappingURL=…` line, which names
|
|
17
|
+
* a map the tree does not hold.
|
|
18
|
+
*
|
|
19
|
+
* The closure is walked here, at generation time, from {@link DERIVATION_ROOTS}
|
|
20
|
+
* over whichever `@aventara/core` this package resolves — so the output is a
|
|
21
|
+
* function of the core version the generator depends on, as the rest of it is a
|
|
22
|
+
* function of the generator. A closure that names a package, carries a
|
|
23
|
+
* triple-slash reference, or leaves core's declarations is refused: it cannot be
|
|
24
|
+
* transported, and core's own gate (`emittable-closure.gate.spec.ts`) exists so
|
|
25
|
+
* that it never is.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The modules whose declarations the generated client copies — the roots of core's
|
|
29
|
+
* `emittable-closure.gate.spec.ts`, mirrored (core cannot export a test constant,
|
|
30
|
+
* and this package cannot read core's tests).
|
|
31
|
+
*/
|
|
32
|
+
export const DERIVATION_ROOTS = [
|
|
33
|
+
"contracts/contract",
|
|
34
|
+
"contracts/scalar-value-type",
|
|
35
|
+
"operations/operation-arguments",
|
|
36
|
+
"operations/operation-call",
|
|
37
|
+
"operations/operation-identity",
|
|
38
|
+
"operations/operation-result",
|
|
39
|
+
"transactions/deferred-operation",
|
|
40
|
+
"transactions/transaction-reference",
|
|
41
|
+
];
|
|
42
|
+
/** The published declarations of the `@aventara/core` this package resolves. */
|
|
43
|
+
export function publishedCoreDeclarations() {
|
|
44
|
+
const dist = path.dirname(fileURLToPath(import.meta.resolve("@aventara/core")));
|
|
45
|
+
return (relative) => {
|
|
46
|
+
try {
|
|
47
|
+
return readFileSync(path.join(dist, relative), "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code === "ENOENT") {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Core's declaration closure from {@link DERIVATION_ROOTS}, as modules under
|
|
59
|
+
* `derivation/`, in UTF-16 code-unit order of their path.
|
|
60
|
+
*
|
|
61
|
+
* @throws Error when the closure cannot be transported — a defect of the
|
|
62
|
+
* installed `@aventara/core`, never of the consumer's input.
|
|
63
|
+
*/
|
|
64
|
+
export function emitDerivationModules(read = publishedCoreDeclarations()) {
|
|
65
|
+
const copied = new Map();
|
|
66
|
+
const queue = DERIVATION_ROOTS.map((root) => `${root}.d.ts`);
|
|
67
|
+
while (queue.length > 0) {
|
|
68
|
+
const file = queue.shift();
|
|
69
|
+
if (copied.has(file)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const text = read(file);
|
|
73
|
+
if (text === undefined) {
|
|
74
|
+
throw new Error(`@aventara/core's declaration closure names ${file}, which it does not publish.`);
|
|
75
|
+
}
|
|
76
|
+
const scanned = scanDeclaration(text);
|
|
77
|
+
if (scanned.references) {
|
|
78
|
+
throw untransportable(`${file} carries a triple-slash reference, which the emitted tree cannot satisfy`);
|
|
79
|
+
}
|
|
80
|
+
for (const specifier of scanned.specifiers) {
|
|
81
|
+
queue.push(declarationTarget(file, specifier));
|
|
82
|
+
}
|
|
83
|
+
copied.set(file, withoutSourceMapTrailer(text));
|
|
84
|
+
}
|
|
85
|
+
return [...copied.keys()]
|
|
86
|
+
.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))
|
|
87
|
+
.map((file) => ({
|
|
88
|
+
path: `derivation/${file}`,
|
|
89
|
+
source: copied.get(file),
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
function untransportable(reason) {
|
|
93
|
+
return new Error(`@aventara/core's declarations cannot be copied into the generated client: ${reason}.`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The declaration file a specifier names, relative to core's `dist`: a relative
|
|
97
|
+
* `.js` specifier inside it, as NodeNext resolves an emitted declaration's import.
|
|
98
|
+
*/
|
|
99
|
+
function declarationTarget(file, specifier) {
|
|
100
|
+
const leaves = () => untransportable(`${file} names ${JSON.stringify(specifier)}, which is not a declaration file beside it`);
|
|
101
|
+
if (!(specifier.startsWith("./") || specifier.startsWith("../")) ||
|
|
102
|
+
!specifier.endsWith(".js")) {
|
|
103
|
+
throw leaves();
|
|
104
|
+
}
|
|
105
|
+
const target = path.posix.normalize(path.posix.join(path.posix.dirname(file), specifier));
|
|
106
|
+
if (target.startsWith("../")) {
|
|
107
|
+
throw leaves();
|
|
108
|
+
}
|
|
109
|
+
return target.replace(/\.js$/, ".d.ts");
|
|
110
|
+
}
|
|
111
|
+
/** `text` without its final `//# sourceMappingURL=…` line, when it ends with one. */
|
|
112
|
+
function withoutSourceMapTrailer(text) {
|
|
113
|
+
return text.replace(/(^|\n)\/\/# sourceMappingURL=[^\n]*\n?$/, "$1");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A word followed directly by a string literal names a module. A pattern, not two
|
|
117
|
+
* string comparisons: the packaging gate reads shipped JavaScript lexically, and
|
|
118
|
+
* a keyword spelled as a quoted literal would read to it as an import.
|
|
119
|
+
*/
|
|
120
|
+
const SPECIFIER_KEYWORD = /^(?:from|import)$/;
|
|
121
|
+
/**
|
|
122
|
+
* The specifiers one declaration file names, read by a small lexer rather than
|
|
123
|
+
* by `typescript` (an optional peer, absent from a consumer's install): comments,
|
|
124
|
+
* string literals and template literals are skipped as units, so text inside them
|
|
125
|
+
* is never mistaken for an import. Agrees with TypeScript's own pre-processor
|
|
126
|
+
* over core's closure (`derivation.emitter.spec.ts`).
|
|
127
|
+
*/
|
|
128
|
+
function scanDeclaration(text) {
|
|
129
|
+
const tokens = [];
|
|
130
|
+
let references = false;
|
|
131
|
+
let index = 0;
|
|
132
|
+
const readQuoted = (quote) => {
|
|
133
|
+
let value = "";
|
|
134
|
+
index += 1;
|
|
135
|
+
while (index < text.length && text[index] !== quote) {
|
|
136
|
+
if (text[index] === "\\") {
|
|
137
|
+
value += text[index + 1] ?? "";
|
|
138
|
+
index += 2;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
value += text[index];
|
|
142
|
+
index += 1;
|
|
143
|
+
}
|
|
144
|
+
index += 1;
|
|
145
|
+
return value;
|
|
146
|
+
};
|
|
147
|
+
/** Scans code until the end, or until the `}` closing a `${` when `inPlaceholder`. */
|
|
148
|
+
const scanCode = (inPlaceholder) => {
|
|
149
|
+
let depth = 0;
|
|
150
|
+
while (index < text.length) {
|
|
151
|
+
const character = text[index];
|
|
152
|
+
const next = text[index + 1];
|
|
153
|
+
if (character === "/" && next === "/") {
|
|
154
|
+
const end = text.indexOf("\n", index);
|
|
155
|
+
const line = text.slice(index, end === -1 ? text.length : end);
|
|
156
|
+
if (/^\/\/\/\s*<reference\b/.test(line)) {
|
|
157
|
+
references = true;
|
|
158
|
+
}
|
|
159
|
+
index = end === -1 ? text.length : end + 1;
|
|
160
|
+
}
|
|
161
|
+
else if (character === "/" && next === "*") {
|
|
162
|
+
const end = text.indexOf("*/", index + 2);
|
|
163
|
+
index = end === -1 ? text.length : end + 2;
|
|
164
|
+
}
|
|
165
|
+
else if (character === '"' || character === "'") {
|
|
166
|
+
tokens.push({ kind: "string", value: readQuoted(character) });
|
|
167
|
+
}
|
|
168
|
+
else if (character === "`") {
|
|
169
|
+
scanTemplate();
|
|
170
|
+
}
|
|
171
|
+
else if (/[A-Za-z_$]/.test(character)) {
|
|
172
|
+
const start = index;
|
|
173
|
+
while (index < text.length && /[\w$]/.test(text[index])) {
|
|
174
|
+
index += 1;
|
|
175
|
+
}
|
|
176
|
+
tokens.push({ kind: "word", value: text.slice(start, index) });
|
|
177
|
+
}
|
|
178
|
+
else if (/\s/.test(character)) {
|
|
179
|
+
index += 1;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
if (inPlaceholder && character === "{") {
|
|
183
|
+
depth += 1;
|
|
184
|
+
}
|
|
185
|
+
else if (inPlaceholder && character === "}") {
|
|
186
|
+
if (depth === 0) {
|
|
187
|
+
index += 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
depth -= 1;
|
|
191
|
+
}
|
|
192
|
+
tokens.push({ kind: "punctuation", value: character });
|
|
193
|
+
index += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const scanTemplate = () => {
|
|
198
|
+
index += 1;
|
|
199
|
+
while (index < text.length && text[index] !== "`") {
|
|
200
|
+
if (text[index] === "\\") {
|
|
201
|
+
index += 2;
|
|
202
|
+
}
|
|
203
|
+
else if (text[index] === "$" && text[index + 1] === "{") {
|
|
204
|
+
index += 2;
|
|
205
|
+
scanCode(true);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
index += 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
index += 1;
|
|
212
|
+
// A template is one opaque token: nothing before it pairs with it.
|
|
213
|
+
tokens.push({ kind: "punctuation", value: "`" });
|
|
214
|
+
};
|
|
215
|
+
scanCode(false);
|
|
216
|
+
const specifiers = [];
|
|
217
|
+
tokens.forEach((token, at) => {
|
|
218
|
+
const following = tokens[at + 1];
|
|
219
|
+
if (token.kind !== "word") {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (SPECIFIER_KEYWORD.test(token.value) && following?.kind === "string") {
|
|
223
|
+
specifiers.push(following.value);
|
|
224
|
+
}
|
|
225
|
+
else if (token.value === "import" &&
|
|
226
|
+
following?.kind === "punctuation" &&
|
|
227
|
+
following.value === "(" &&
|
|
228
|
+
tokens[at + 2]?.kind === "string") {
|
|
229
|
+
specifiers.push(tokens[at + 2].value);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return { specifiers, references };
|
|
233
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { isOperationVariantAvailable, } from "@aventara/core";
|
|
2
|
+
import { ownPropertyKey } from "./name.deriver.js";
|
|
3
|
+
/**
|
|
4
|
+
* `generated/runtime/descriptor.ts` — what the RUNTIME needs of the ClientContract,
|
|
5
|
+
* as data (P1, P4). A projection of the same accepted contract the carrier
|
|
6
|
+
* (`generated/contract.ts`) holds, derived in the same run, so it is not a second
|
|
7
|
+
* source: `descriptor.emitter.spec.ts` regenerates it from the parsed carrier and
|
|
8
|
+
* compares.
|
|
9
|
+
*
|
|
10
|
+
* - **The decode table (P1).** Encoding is decidable by a value's runtime class;
|
|
11
|
+
* decoding is not — a wire `"12"` is a `string`, a `bigint` or a `decimal`
|
|
12
|
+
* depending only on the field (M13). So per Resource the table names each
|
|
13
|
+
* result field whose wire string is revived (`bigint`, `decimal`, `datetime`,
|
|
14
|
+
* `bytes`, §6.2) and each relation with its target and whether it is to-many,
|
|
15
|
+
* which the decode walk recurses into (S5 added `many`: a to-one record may
|
|
16
|
+
* itself hold a `data` and a `count` field, so the shape alone cannot say). Every other key passes through untouched; `json` is never
|
|
17
|
+
* revived.
|
|
18
|
+
* - **The advertised operations (P4).** Read here through core's
|
|
19
|
+
* `isOperationVariantAvailable`, emitted as `[resource, family, variant]`; the
|
|
20
|
+
* type grammar reads the carrier's `operations` keys. They agree while present
|
|
21
|
+
* ⇔ advertised (Phase 10 A3), and a gate asserts it over both pilots.
|
|
22
|
+
*
|
|
23
|
+
* Every registry is walked in UTF-16 code-unit order, so the key order a
|
|
24
|
+
* contract arrives in never reaches the bytes (C-843).
|
|
25
|
+
*/
|
|
26
|
+
/** The scalars whose wire form a decoder revives into another runtime class (§6.2). */
|
|
27
|
+
const REVIVED_SCALARS = new Set([
|
|
28
|
+
"bigint",
|
|
29
|
+
"bytes",
|
|
30
|
+
"datetime",
|
|
31
|
+
"decimal",
|
|
32
|
+
]);
|
|
33
|
+
/** `generated/runtime/descriptor.ts`, before the banner. */
|
|
34
|
+
export function emitDescriptorModule(contract) {
|
|
35
|
+
const resources = codeUnitOrder(Object.keys(contract.resources));
|
|
36
|
+
const table = resources.map((name) => {
|
|
37
|
+
const fields = contract.resources[name]?.fields ?? {};
|
|
38
|
+
const entries = codeUnitOrder(Object.keys(fields)).flatMap((field) => {
|
|
39
|
+
const decoding = decodingOf(fields[field]);
|
|
40
|
+
return decoding === undefined
|
|
41
|
+
? []
|
|
42
|
+
: [`${ownPropertyKey(field)}: ${decoding}`];
|
|
43
|
+
});
|
|
44
|
+
return `\t${ownPropertyKey(name)}: {${entries.length === 0 ? "" : ` ${entries.join(", ")} `}},\n`;
|
|
45
|
+
});
|
|
46
|
+
const advertised = resources.flatMap((name) => {
|
|
47
|
+
const operations = contract.resources[name]?.operations ?? {};
|
|
48
|
+
return codeUnitOrder(Object.keys(operations)).flatMap((family) => {
|
|
49
|
+
const variants = (operations[family] ?? {});
|
|
50
|
+
return codeUnitOrder(Object.keys(variants))
|
|
51
|
+
.filter((variant) => isOperationVariantAvailable(variants[variant]))
|
|
52
|
+
.map((variant) => `\t[${[name, family, variant].map((part) => JSON.stringify(part)).join(", ")}],\n`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
path: "runtime/descriptor.ts",
|
|
57
|
+
source: "/**\n" +
|
|
58
|
+
" * A result field the decoder revives from its wire string (§6.2), or a relation\n" +
|
|
59
|
+
" * it recurses into, read as the relation's target Resource — `many` saying the\n" +
|
|
60
|
+
" * value is a list, `{ data, count }` or `{ count }` rather than one record (P1).\n" +
|
|
61
|
+
" */\n" +
|
|
62
|
+
"export type FieldDecoding =\n" +
|
|
63
|
+
'\t| "bigint"\n' +
|
|
64
|
+
'\t| "bytes"\n' +
|
|
65
|
+
'\t| "datetime"\n' +
|
|
66
|
+
'\t| "decimal"\n' +
|
|
67
|
+
"\t| { readonly relation: string; readonly many: boolean };\n" +
|
|
68
|
+
"\n" +
|
|
69
|
+
"/** Per Resource, the result fields that need decoding; any other key passes through. */\n" +
|
|
70
|
+
"export const DECODE_TABLE: {\n" +
|
|
71
|
+
"\treadonly [resource: string]: { readonly [field: string]: FieldDecoding };\n" +
|
|
72
|
+
`} = {${table.length === 0 ? "" : `\n${table.join("")}`}};\n` +
|
|
73
|
+
"\n" +
|
|
74
|
+
"/** The operations the ClientContract advertises, as `[resource, family, variant]`. */\n" +
|
|
75
|
+
"export const ADVERTISED_OPERATIONS: readonly (readonly [\n" +
|
|
76
|
+
"\tresource: string,\n" +
|
|
77
|
+
"\tfamily: string,\n" +
|
|
78
|
+
"\tvariant: string,\n" +
|
|
79
|
+
`])[] = [${advertised.length === 0 ? "" : `\n${advertised.join("")}`}];\n`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** The table entry a field needs, as source, or `undefined` when it passes through. */
|
|
83
|
+
function decodingOf(field) {
|
|
84
|
+
if (field === undefined) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (field.kind === "relation") {
|
|
88
|
+
return `{ relation: ${JSON.stringify(field.target)}, many: ${field.cardinality === "many"} }`;
|
|
89
|
+
}
|
|
90
|
+
return "scalar" in field.type &&
|
|
91
|
+
REVIVED_SCALARS.has(field.type.scalar)
|
|
92
|
+
? JSON.stringify(field.type.scalar)
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
|
95
|
+
function codeUnitOrder(values) {
|
|
96
|
+
return [...values].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
97
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generator's output as a VALUE before it is a filesystem effect (plan §7,
|
|
3
|
+
* group 3). Byte-equality is therefore testable without touching disk, and
|
|
4
|
+
* temp → validate → replace (S7) is a property of the writer alone rather than of
|
|
5
|
+
* every emitter.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The output layout (architect, 2026-10-04): the consumer names a directory,
|
|
9
|
+
* `generateAt`, which is SHARED — they may keep their own files there. The
|
|
10
|
+
* generator owns exactly two entries in it:
|
|
11
|
+
*
|
|
12
|
+
* - `AvClient.ts`, the entry point a consumer imports;
|
|
13
|
+
* - `generated/`, every other module, owned and replaced whole.
|
|
14
|
+
*/
|
|
15
|
+
/** The entry point, at the root of `generateAt`. */
|
|
16
|
+
export declare const CLIENT_ENTRY_FILE = "AvClient.ts";
|
|
17
|
+
/** The directory holding every other emitted module, under `generateAt`. */
|
|
18
|
+
export declare const GENERATED_DIRECTORY = "generated";
|
|
19
|
+
/**
|
|
20
|
+
* One of core's published declaration files, copied under `generated/derivation/`
|
|
21
|
+
* at its path relative to core's `dist` (Q1 = A, `derivation.emitter.ts`).
|
|
22
|
+
*/
|
|
23
|
+
export type DerivationModulePath = `derivation/${string}.d.ts`;
|
|
24
|
+
/**
|
|
25
|
+
* The modules under `generated/`: the emitted sources, the carrier
|
|
26
|
+
* (`contract.ts`, Q4), the typed surface (`client.ts`) and named types
|
|
27
|
+
* (`types.d.ts`, S4 — a declaration file, architect 2026-10-05), and core's declarations under `derivation/`. §15.4's
|
|
28
|
+
* `runtime/transaction.ts` and `runtime/fingerprint.ts` exist iff transactions
|
|
29
|
+
* are interactive (S6, P3); `resources/` and `runtime/projection.ts` are not
|
|
30
|
+
* emitted (plan §7).
|
|
31
|
+
*
|
|
32
|
+
* Relative to `generated/`, POSIX-separated.
|
|
33
|
+
*/
|
|
34
|
+
export type GeneratedModulePath = DerivationModulePath | "client.ts" | "contract.ts" | "enums.ts" | "metadata.ts" | "runtime/codec.ts" | "runtime/decimal.ts" | "runtime/descriptor.ts" | "runtime/errors.ts" | "runtime/fingerprint.ts" | "runtime/transaction.ts" | "runtime/transport.ts" | "types.d.ts";
|
|
35
|
+
/** A path in the emitted tree: relative to `generateAt`, POSIX-separated. */
|
|
36
|
+
export type EmittedFilePath = typeof CLIENT_ENTRY_FILE | `${typeof GENERATED_DIRECTORY}/${GeneratedModulePath}`;
|
|
37
|
+
/** One module's TypeScript source under `generated/`, before the banner and before encoding. */
|
|
38
|
+
export interface EmittedModule {
|
|
39
|
+
readonly path: GeneratedModulePath;
|
|
40
|
+
readonly source: string;
|
|
41
|
+
}
|
|
42
|
+
/** One emitted file: its path and its exact bytes, banner included, UTF-8. */
|
|
43
|
+
export interface EmittedFile {
|
|
44
|
+
readonly path: EmittedFilePath;
|
|
45
|
+
readonly bytes: Uint8Array;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Every file one generation emits, in UTF-16 code-unit order of `path`, each path
|
|
49
|
+
* once — `AvClient.ts` first, then `generated/**`. Replaced, never merged
|
|
50
|
+
* (§15.3): `generated/` whole, `AvClient.ts` as one file.
|
|
51
|
+
*/
|
|
52
|
+
export type EmittedTree = readonly EmittedFile[];
|
|
53
|
+
/**
|
|
54
|
+
* What emission yields: the tree, and the diagnostics a run raised without
|
|
55
|
+
* failing — today, one line per renamed name (`name.deriver.ts`). Returned rather
|
|
56
|
+
* than printed, so the code that owns the terminal decides where they go.
|
|
57
|
+
*/
|
|
58
|
+
export interface ClientEmission {
|
|
59
|
+
readonly tree: EmittedTree;
|
|
60
|
+
readonly warnings: readonly string[];
|
|
61
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generator's output as a VALUE before it is a filesystem effect (plan §7,
|
|
3
|
+
* group 3). Byte-equality is therefore testable without touching disk, and
|
|
4
|
+
* temp → validate → replace (S7) is a property of the writer alone rather than of
|
|
5
|
+
* every emitter.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The output layout (architect, 2026-10-04): the consumer names a directory,
|
|
9
|
+
* `generateAt`, which is SHARED — they may keep their own files there. The
|
|
10
|
+
* generator owns exactly two entries in it:
|
|
11
|
+
*
|
|
12
|
+
* - `AvClient.ts`, the entry point a consumer imports;
|
|
13
|
+
* - `generated/`, every other module, owned and replaced whole.
|
|
14
|
+
*/
|
|
15
|
+
/** The entry point, at the root of `generateAt`. */
|
|
16
|
+
export const CLIENT_ENTRY_FILE = "AvClient.ts";
|
|
17
|
+
/** The directory holding every other emitted module, under `generateAt`. */
|
|
18
|
+
export const GENERATED_DIRECTORY = "generated";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ClientContract } from "@aventara/core";
|
|
2
|
+
import type { EmittedModule } from "./emitted-tree.interface.js";
|
|
3
|
+
import { type EmittedNames } from "./name.deriver.js";
|
|
4
|
+
/**
|
|
5
|
+
* `enums.ts`: per enum, a string-literal union type and a same-named `as const`
|
|
6
|
+
* object (§15.4's "enum types"; architect decision, 2026-10-04):
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* export type Role = "ADMIN" | "USER";
|
|
10
|
+
* export const Role = { ADMIN: "ADMIN", USER: "USER" } as const;
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* The type is what every later emitter references; the object gives a consumer a
|
|
14
|
+
* value to name a member by and to enumerate. Both are declared under the enum's
|
|
15
|
+
* emitted identifier, which differs from its contract name only when the name was
|
|
16
|
+
* renamed (`name.deriver.ts`). The values are the wire values and are never
|
|
17
|
+
* renamed.
|
|
18
|
+
*
|
|
19
|
+
* Enums come in the order `names` gives them — UTF-16 code units of the contract
|
|
20
|
+
* name — so the key order a contract arrives in never reaches the bytes (C-843).
|
|
21
|
+
* An enum's VALUES keep their declared order, in the type and in the object: that
|
|
22
|
+
* order is meaning, and canonical form keeps it too.
|
|
23
|
+
*/
|
|
24
|
+
export declare function emitEnumsModule(contract: Pick<ClientContract, "enums">, names: EmittedNames): EmittedModule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ownPropertyKey } from "./name.deriver.js";
|
|
2
|
+
/**
|
|
3
|
+
* `enums.ts`: per enum, a string-literal union type and a same-named `as const`
|
|
4
|
+
* object (§15.4's "enum types"; architect decision, 2026-10-04):
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* export type Role = "ADMIN" | "USER";
|
|
8
|
+
* export const Role = { ADMIN: "ADMIN", USER: "USER" } as const;
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* The type is what every later emitter references; the object gives a consumer a
|
|
12
|
+
* value to name a member by and to enumerate. Both are declared under the enum's
|
|
13
|
+
* emitted identifier, which differs from its contract name only when the name was
|
|
14
|
+
* renamed (`name.deriver.ts`). The values are the wire values and are never
|
|
15
|
+
* renamed.
|
|
16
|
+
*
|
|
17
|
+
* Enums come in the order `names` gives them — UTF-16 code units of the contract
|
|
18
|
+
* name — so the key order a contract arrives in never reaches the bytes (C-843).
|
|
19
|
+
* An enum's VALUES keep their declared order, in the type and in the object: that
|
|
20
|
+
* order is meaning, and canonical form keeps it too.
|
|
21
|
+
*/
|
|
22
|
+
export function emitEnumsModule(contract, names) {
|
|
23
|
+
const declarations = names.enums.map(({ contractName, identifier }) => {
|
|
24
|
+
const values = contract.enums[contractName]?.values ?? [];
|
|
25
|
+
const union = values.length === 0
|
|
26
|
+
? "never"
|
|
27
|
+
: values.map((value) => JSON.stringify(value)).join(" | ");
|
|
28
|
+
const members = values.length === 0
|
|
29
|
+
? "{}"
|
|
30
|
+
: `{ ${values.map((value) => `${ownPropertyKey(value)}: ${JSON.stringify(value)}`).join(", ")} }`;
|
|
31
|
+
return (`export type ${identifier} = ${union};\n` +
|
|
32
|
+
`export const ${identifier} = ${members} as const;\n`);
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
path: "enums.ts",
|
|
36
|
+
// Without an import or export, a consumer whose config treats such a file
|
|
37
|
+
// as a script (`moduleResolution: "bundler"` outside a `"type": "module"`
|
|
38
|
+
// package, measured) fails `AvClient.ts`'s `export *` with TS2306 — so a
|
|
39
|
+
// contract without enums still emits a module.
|
|
40
|
+
source: declarations.length === 0 ? "export {};\n" : declarations.join("\n"),
|
|
41
|
+
};
|
|
42
|
+
}
|