@aventara/client 0.0.0-stage → 0.1.0-pilot.1
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 +234 -2
- package/dist/avclient.bin.d.ts +2 -0
- package/dist/avclient.bin.js +15 -0
- package/dist/cli/command.parser.d.ts +37 -0
- package/dist/cli/command.parser.js +177 -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 +52 -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 +75 -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 +41 -0
- package/dist/config/config.loader.js +95 -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 +11 -0
- package/dist/init/client-config.template.js +27 -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 +86 -0
- package/dist/init/client-init.planner.d.ts +27 -0
- package/dist/init/client-init.planner.js +99 -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 +75 -0
- package/dist/output/output.validator.js +262 -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,75 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
import type { EmittedTree } from "../emit/emitted-tree.interface.js";
|
|
3
|
+
/**
|
|
4
|
+
* §15.3's "type/sanity validation" step, between emit-to-temp and the replace:
|
|
5
|
+
* the gate that makes the atomicity worth having (plan §4 Q6). It judges a
|
|
6
|
+
* directory the writer has just filled with `tree`, and answers with a value — the
|
|
7
|
+
* writer decides what a rejection means for the previous output.
|
|
8
|
+
*
|
|
9
|
+
* # Three checks, the strongest available first
|
|
10
|
+
*
|
|
11
|
+
* - **Shape**, always: the directory holds exactly `tree` — no file missing, none
|
|
12
|
+
* extra, every one byte-for-byte what was emitted — and every file carries the
|
|
13
|
+
* ownership line. That last is not decoration: the NEXT run reads it to decide
|
|
14
|
+
* that this directory is a previous generation it may replace whole, so a file
|
|
15
|
+
* without it would make the next run refuse.
|
|
16
|
+
* - **Types**, when the `typescript` optional peer resolves (Q5, Q6): a real
|
|
17
|
+
* program over the tree under a consumer's strictest plausible settings. The
|
|
18
|
+
* tree is judged ALONE — the compiler host serves nothing outside the directory
|
|
19
|
+
* but TypeScript's own `lib` files, so an import the tree cannot satisfy itself
|
|
20
|
+
* fails here even when a `node_modules` beside the output could satisfy it
|
|
21
|
+
* (§15.5).
|
|
22
|
+
* - **Syntax**, when it does not — or when the `typescript` that resolves has no
|
|
23
|
+
* classic compiler API (TypeScript 7, plan B3; pilot.1): each file through
|
|
24
|
+
* Node's own TypeScript parser (`node:module`'s `stripTypeScriptTypes`), with a
|
|
25
|
+
* loud warning that the output was NOT type-checked. Degraded, never skipped
|
|
26
|
+
* (S7), and never a refusal: the developer's own `tsc` still checks the tree
|
|
27
|
+
* when their project compiles.
|
|
28
|
+
*/
|
|
29
|
+
/** The `typescript` module, as the validator uses it. */
|
|
30
|
+
export type TypeScriptCompiler = typeof ts;
|
|
31
|
+
/**
|
|
32
|
+
* Finds the `typescript` optional peer: the module, or `undefined` when it is not
|
|
33
|
+
* installed. Anything else — an installation that resolves but fails to load — is
|
|
34
|
+
* not "absent" and is thrown.
|
|
35
|
+
*/
|
|
36
|
+
export type TypeScriptResolver = () => Promise<TypeScriptCompiler | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* A resolver for `specifier`, looked up from this package the way Node would look
|
|
39
|
+
* it up: an optional peer is linked beside the package that declares it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function typeScriptResolverFor(specifier: string): TypeScriptResolver;
|
|
42
|
+
/** The `typescript` this package's optional peer names (Q5). */
|
|
43
|
+
export declare const resolveInstalledTypeScript: TypeScriptResolver;
|
|
44
|
+
/** How deeply the tree was judged. */
|
|
45
|
+
export type OutputCheck = "types" | "syntax" | "shape";
|
|
46
|
+
export interface OutputAccepted {
|
|
47
|
+
readonly accepted: true;
|
|
48
|
+
readonly checked: OutputCheck;
|
|
49
|
+
/** Said once to the person running the generator; empty when nothing degraded. */
|
|
50
|
+
readonly warnings: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
export interface OutputRejected {
|
|
53
|
+
readonly accepted: false;
|
|
54
|
+
readonly checked: OutputCheck;
|
|
55
|
+
/** What is wrong, one line per finding, paths relative to the tree. */
|
|
56
|
+
readonly findings: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
export type OutputValidation = OutputAccepted | OutputRejected;
|
|
59
|
+
/**
|
|
60
|
+
* The warning a run without `typescript` carries. Loud on purpose, in its words:
|
|
61
|
+
* the prefix is the CLI's one `warning:`, so the text carries none of its own.
|
|
62
|
+
*/
|
|
63
|
+
export declare const TYPESCRIPT_UNRESOLVED_WARNING: string;
|
|
64
|
+
/** The warning a run with neither `typescript` nor Node's parser carries. */
|
|
65
|
+
export declare const SYNTAX_CHECK_UNAVAILABLE_WARNING: string;
|
|
66
|
+
/**
|
|
67
|
+
* The warning a run carries when the `typescript` that resolves is one the
|
|
68
|
+
* generator cannot type-check with (TypeScript 7): what was checked instead,
|
|
69
|
+
* and where the type check still happens.
|
|
70
|
+
*/
|
|
71
|
+
export declare function typeScriptWithoutCompilerApiWarning(version: string, checked: "syntax" | "shape"): string;
|
|
72
|
+
/**
|
|
73
|
+
* Judges `directory`, which the caller has just filled with `tree`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function validateOutputTree(directory: string, tree: EmittedTree, resolveTypeScript?: TypeScriptResolver): Promise<OutputValidation>;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import * as nodeModule from "node:module";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { carriesGeneratedOwnership } from "../emit/banner.emitter.js";
|
|
7
|
+
/**
|
|
8
|
+
* A resolver for `specifier`, looked up from this package the way Node would look
|
|
9
|
+
* it up: an optional peer is linked beside the package that declares it.
|
|
10
|
+
*/
|
|
11
|
+
export function typeScriptResolverFor(specifier) {
|
|
12
|
+
return async () => {
|
|
13
|
+
let resolved;
|
|
14
|
+
try {
|
|
15
|
+
resolved = createRequire(import.meta.url).resolve(specifier);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (isErrorWithCode(error, "MODULE_NOT_FOUND")) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const loaded = (await import(pathToFileURL(resolved).href));
|
|
24
|
+
return loaded.default ?? loaded;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The `typescript` this package's optional peer names (Q5). */
|
|
28
|
+
export const resolveInstalledTypeScript = typeScriptResolverFor("typescript");
|
|
29
|
+
/**
|
|
30
|
+
* Whether `compiler` has the classic compiler API {@link typeFindingsOf} uses.
|
|
31
|
+
* TypeScript 7 does not (plan B3: its package exports its version and nothing
|
|
32
|
+
* else); before pilot.1 the generator refused it, and its peer range stopped
|
|
33
|
+
* below it — which made `npm i` fail with ERESOLVE in any frontend whose
|
|
34
|
+
* `typescript` is the current `latest`.
|
|
35
|
+
*/
|
|
36
|
+
function hasClassicCompilerApi(compiler) {
|
|
37
|
+
// One member, not `Partial<typeof ts>`: a mapped type over the whole
|
|
38
|
+
// compiler namespace costs thousands of instantiations to ask one question.
|
|
39
|
+
return (typeof compiler.createProgram ===
|
|
40
|
+
"function");
|
|
41
|
+
}
|
|
42
|
+
/** The version a resolved `typescript` names, whatever its API. */
|
|
43
|
+
function versionOf(compiler) {
|
|
44
|
+
const { version } = compiler;
|
|
45
|
+
return typeof version === "string" ? version : "(no version)";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The warning a run without `typescript` carries. Loud on purpose, in its words:
|
|
49
|
+
* the prefix is the CLI's one `warning:`, so the text carries none of its own.
|
|
50
|
+
*/
|
|
51
|
+
export const TYPESCRIPT_UNRESOLVED_WARNING = "`typescript` could not be resolved, so the generated output was NOT type-checked — " +
|
|
52
|
+
"only its shape and syntax were. Install `typescript` (an optional peer of @aventara/client) " +
|
|
53
|
+
"in this project to restore the type check.";
|
|
54
|
+
/** The warning a run with neither `typescript` nor Node's parser carries. */
|
|
55
|
+
export const SYNTAX_CHECK_UNAVAILABLE_WARNING = "neither `typescript` nor Node's TypeScript parser is available, so the generated " +
|
|
56
|
+
"output was NOT type-checked or parsed — only its shape was. Install `typescript` (an " +
|
|
57
|
+
"optional peer of @aventara/client) in this project to restore the type check.";
|
|
58
|
+
/**
|
|
59
|
+
* The warning a run carries when the `typescript` that resolves is one the
|
|
60
|
+
* generator cannot type-check with (TypeScript 7): what was checked instead,
|
|
61
|
+
* and where the type check still happens.
|
|
62
|
+
*/
|
|
63
|
+
export function typeScriptWithoutCompilerApiWarning(version, checked) {
|
|
64
|
+
return (`the installed \`typescript\` ${version} has no classic compiler API (\`createProgram\`), so the generated ` +
|
|
65
|
+
`output was NOT type-checked${checked === "shape" ? " or parsed — only its shape was" : " — only its shape and syntax were"}. ` +
|
|
66
|
+
"Your project's own `tsc` checks it when it compiles; a `typescript` 5.5 to 6 restores the generator's own type check.");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Judges `directory`, which the caller has just filled with `tree`.
|
|
70
|
+
*/
|
|
71
|
+
export async function validateOutputTree(directory, tree, resolveTypeScript = resolveInstalledTypeScript) {
|
|
72
|
+
const shapeFindings = await shapeFindingsOf(directory, tree);
|
|
73
|
+
if (shapeFindings.length > 0) {
|
|
74
|
+
return { accepted: false, checked: "shape", findings: shapeFindings };
|
|
75
|
+
}
|
|
76
|
+
const compiler = await resolveTypeScript();
|
|
77
|
+
if (compiler !== undefined && hasClassicCompilerApi(compiler)) {
|
|
78
|
+
const findings = typeFindingsOf(compiler, directory, tree);
|
|
79
|
+
return findings.length > 0
|
|
80
|
+
? { accepted: false, checked: "types", findings }
|
|
81
|
+
: { accepted: true, checked: "types", warnings: [] };
|
|
82
|
+
}
|
|
83
|
+
const strip = nodeTypeScriptParser();
|
|
84
|
+
if (strip === undefined) {
|
|
85
|
+
return {
|
|
86
|
+
accepted: true,
|
|
87
|
+
checked: "shape",
|
|
88
|
+
warnings: [
|
|
89
|
+
compiler === undefined
|
|
90
|
+
? SYNTAX_CHECK_UNAVAILABLE_WARNING
|
|
91
|
+
: typeScriptWithoutCompilerApiWarning(versionOf(compiler), "shape"),
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const findings = syntaxFindingsOf(strip, tree);
|
|
96
|
+
return findings.length > 0
|
|
97
|
+
? { accepted: false, checked: "syntax", findings }
|
|
98
|
+
: {
|
|
99
|
+
accepted: true,
|
|
100
|
+
checked: "syntax",
|
|
101
|
+
warnings: [
|
|
102
|
+
compiler === undefined
|
|
103
|
+
? TYPESCRIPT_UNRESOLVED_WARNING
|
|
104
|
+
: typeScriptWithoutCompilerApiWarning(versionOf(compiler), "syntax"),
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/* ------------------------------------------------------------------ *
|
|
109
|
+
* Shape *
|
|
110
|
+
* ------------------------------------------------------------------ */
|
|
111
|
+
async function shapeFindingsOf(directory, tree) {
|
|
112
|
+
const findings = [];
|
|
113
|
+
const expected = new Map(tree.map((file) => [file.path, file.bytes]));
|
|
114
|
+
const entries = await readdir(directory, {
|
|
115
|
+
recursive: true,
|
|
116
|
+
withFileTypes: true,
|
|
117
|
+
});
|
|
118
|
+
const present = new Set();
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const relative = path
|
|
124
|
+
.relative(directory, path.join(entry.parentPath, entry.name))
|
|
125
|
+
.split(path.sep)
|
|
126
|
+
.join("/");
|
|
127
|
+
present.add(relative);
|
|
128
|
+
if (!entry.isFile() || !expected.has(relative)) {
|
|
129
|
+
findings.push(`${relative}: not part of the emitted tree`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
133
|
+
for (const [relative, bytes] of expected) {
|
|
134
|
+
if (!present.has(relative)) {
|
|
135
|
+
findings.push(`${relative}: missing`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const written = await readFile(path.join(directory, relative));
|
|
139
|
+
if (!Buffer.from(bytes).equals(written)) {
|
|
140
|
+
findings.push(`${relative}: not the bytes that were emitted`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
let text;
|
|
144
|
+
try {
|
|
145
|
+
text = decoder.decode(written);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
findings.push(`${relative}: not UTF-8`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!carriesGeneratedOwnership(text)) {
|
|
152
|
+
findings.push(`${relative}: does not carry the generated banner`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return findings.sort();
|
|
156
|
+
}
|
|
157
|
+
/* ------------------------------------------------------------------ *
|
|
158
|
+
* Types: a real program over the tree, and nothing outside it *
|
|
159
|
+
* ------------------------------------------------------------------ */
|
|
160
|
+
/**
|
|
161
|
+
* The emitted-tree fixture's settings (`__fixtures__/emitted-tree.fixture.ts`),
|
|
162
|
+
* restated here because production code does not import a test fixture: a
|
|
163
|
+
* consumer's strictest plausible configuration, with no DOM and no Node (§15.7:
|
|
164
|
+
* both are first-class targets, so the tree may assume neither).
|
|
165
|
+
*/
|
|
166
|
+
function compilerOptionsFor(compiler) {
|
|
167
|
+
return {
|
|
168
|
+
target: compiler.ScriptTarget.ES2022,
|
|
169
|
+
module: compiler.ModuleKind.NodeNext,
|
|
170
|
+
moduleResolution: compiler.ModuleResolutionKind.NodeNext,
|
|
171
|
+
lib: ["lib.es2022.d.ts"],
|
|
172
|
+
types: [],
|
|
173
|
+
strict: true,
|
|
174
|
+
noUncheckedIndexedAccess: true,
|
|
175
|
+
exactOptionalPropertyTypes: true,
|
|
176
|
+
verbatimModuleSyntax: true,
|
|
177
|
+
isolatedModules: true,
|
|
178
|
+
erasableSyntaxOnly: true,
|
|
179
|
+
noUnusedLocals: true,
|
|
180
|
+
noUnusedParameters: true,
|
|
181
|
+
noImplicitReturns: true,
|
|
182
|
+
noImplicitOverride: true,
|
|
183
|
+
noFallthroughCasesInSwitch: true,
|
|
184
|
+
noPropertyAccessFromIndexSignature: true,
|
|
185
|
+
skipDefaultLibCheck: true,
|
|
186
|
+
noEmit: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The tree is served as an ES module package of its own: NodeNext reads the
|
|
191
|
+
* nearest `package.json` to decide a `.ts` file's module format, and the one a
|
|
192
|
+
* consumer's project happens to have must not decide this verdict.
|
|
193
|
+
*/
|
|
194
|
+
const VIRTUAL_PACKAGE_JSON = '{ "type": "module" }';
|
|
195
|
+
function typeFindingsOf(compiler, directory, tree) {
|
|
196
|
+
const root = path.resolve(directory);
|
|
197
|
+
const options = compilerOptionsFor(compiler);
|
|
198
|
+
const libraryDirectory = path.dirname(compiler.getDefaultLibFilePath(options));
|
|
199
|
+
const virtualPackageJson = path.join(root, "package.json");
|
|
200
|
+
const servable = (file) => isInside(root, file) || isInside(libraryDirectory, file);
|
|
201
|
+
const host = compiler.createCompilerHost(options, true);
|
|
202
|
+
const baseFileExists = host.fileExists.bind(host);
|
|
203
|
+
const baseReadFile = host.readFile.bind(host);
|
|
204
|
+
const baseDirectoryExists = host.directoryExists?.bind(host);
|
|
205
|
+
host.fileExists = (file) => path.resolve(file) === virtualPackageJson ||
|
|
206
|
+
(servable(path.resolve(file)) && baseFileExists(file));
|
|
207
|
+
host.readFile = (file) => path.resolve(file) === virtualPackageJson
|
|
208
|
+
? VIRTUAL_PACKAGE_JSON
|
|
209
|
+
: servable(path.resolve(file))
|
|
210
|
+
? baseReadFile(file)
|
|
211
|
+
: undefined;
|
|
212
|
+
host.directoryExists = (candidate) => {
|
|
213
|
+
const resolved = path.resolve(candidate);
|
|
214
|
+
return ((servable(resolved) || isInside(resolved, root)) &&
|
|
215
|
+
(baseDirectoryExists?.(candidate) ?? true));
|
|
216
|
+
};
|
|
217
|
+
host.getCurrentDirectory = () => root;
|
|
218
|
+
const program = compiler.createProgram({
|
|
219
|
+
rootNames: tree.map((file) => path.join(root, file.path)),
|
|
220
|
+
options,
|
|
221
|
+
host,
|
|
222
|
+
});
|
|
223
|
+
const diagnostics = compiler.getPreEmitDiagnostics(program);
|
|
224
|
+
return diagnostics.map((diagnostic) => compiler
|
|
225
|
+
.formatDiagnostic(diagnostic, {
|
|
226
|
+
getCanonicalFileName: (file) => file,
|
|
227
|
+
getCurrentDirectory: () => root,
|
|
228
|
+
getNewLine: () => "\n",
|
|
229
|
+
})
|
|
230
|
+
.trimEnd());
|
|
231
|
+
}
|
|
232
|
+
/** Whether `file` is `directory` or lies beneath it. */
|
|
233
|
+
function isInside(directory, file) {
|
|
234
|
+
const relative = path.relative(directory, file);
|
|
235
|
+
return (relative === "" ||
|
|
236
|
+
(!relative.startsWith("..") && !path.isAbsolute(relative)));
|
|
237
|
+
}
|
|
238
|
+
/** Node's TypeScript parser, where this Node has one. */
|
|
239
|
+
function nodeTypeScriptParser() {
|
|
240
|
+
const candidate = nodeModule.stripTypeScriptTypes;
|
|
241
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
242
|
+
}
|
|
243
|
+
function syntaxFindingsOf(strip, tree) {
|
|
244
|
+
const decoder = new TextDecoder("utf-8");
|
|
245
|
+
const findings = [];
|
|
246
|
+
for (const file of tree) {
|
|
247
|
+
try {
|
|
248
|
+
strip(decoder.decode(file.bytes));
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (isErrorWithCode(error, "ERR_INVALID_TYPESCRIPT_SYNTAX")) {
|
|
252
|
+
findings.push(`${file.path}: ${error.message}`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return findings;
|
|
259
|
+
}
|
|
260
|
+
function isErrorWithCode(error, code) {
|
|
261
|
+
return error instanceof Error && error.code === code;
|
|
262
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as fsPromises from "node:fs/promises";
|
|
2
|
+
import type { AbsolutePath } from "../config/client-config.interface.js";
|
|
3
|
+
import { type ClientEmission, type EmittedTree } from "../emit/emitted-tree.interface.js";
|
|
4
|
+
import { type OutputCheck, type TypeScriptResolver } from "./output.validator.js";
|
|
5
|
+
/**
|
|
6
|
+
* §15.3's last two steps as one call — emit into a temporary directory, validate
|
|
7
|
+
* it, atomically replace the generated output — and the property that makes them
|
|
8
|
+
* one: **a failed generation leaves the previous valid output intact**, byte for
|
|
9
|
+
* byte.
|
|
10
|
+
*
|
|
11
|
+
* # What it owns (architect, 2026-10-04)
|
|
12
|
+
*
|
|
13
|
+
* The output is written into `generateAt`, a directory the developer SHARES: it
|
|
14
|
+
* may hold their own files. The generator owns exactly two entries in it —
|
|
15
|
+
* `AvClient.ts` and `generated/` — and reads, refuses on, moves or deletes
|
|
16
|
+
* nothing else there, apart from the transient entries it names itself
|
|
17
|
+
* (`.aventara-next-*`, `.aventara-ready-*`, `.generated.aventara-previous`).
|
|
18
|
+
*
|
|
19
|
+
* - `generated/` is replaced WHOLE, never merged: a file the previous run emitted
|
|
20
|
+
* and this one does not is gone. It is replaced only when every file in it
|
|
21
|
+
* carries the generated banner's ownership line (an empty directory owns
|
|
22
|
+
* nothing and is fine); otherwise the run is refused, naming the foreign files.
|
|
23
|
+
* - `AvClient.ts` is replaced only when it is absent or carries the ownership
|
|
24
|
+
* line; otherwise the run is refused, naming it.
|
|
25
|
+
*
|
|
26
|
+
* # The order, and its one window
|
|
27
|
+
*
|
|
28
|
+
* The whole tree — `AvClient.ts` and `generated/` together — is written into a
|
|
29
|
+
* staging directory inside `generateAt` (same filesystem, so every move is one
|
|
30
|
+
* atomic `rename`) and validated there as one program. Then:
|
|
31
|
+
*
|
|
32
|
+
* 1. the staging directory is renamed `.aventara-ready-*`: from here it is known
|
|
33
|
+
* to be validated;
|
|
34
|
+
* 2. the previous `generated/` → `.generated.aventara-previous`;
|
|
35
|
+
* 3. the staged `generated/` → `generated/`;
|
|
36
|
+
* 4. the staged `AvClient.ts` → `AvClient.ts`, one rename over the old file;
|
|
37
|
+
* 5. the previous `generated/` and the staging directory are removed.
|
|
38
|
+
*
|
|
39
|
+
* A failed step 3 renames the previous `generated/` straight back; a failed step
|
|
40
|
+
* 4 moves the new `generated/` out again and the previous back, so a failure at
|
|
41
|
+
* any step leaves the previous pair. Node has no atomic exchange of two entries,
|
|
42
|
+
* so two windows remain for a process KILLED mid-replace, and the next run's
|
|
43
|
+
* recovery closes each before it does anything else:
|
|
44
|
+
*
|
|
45
|
+
* - between 2 and 3, `generated/` does not exist: the previous one is put back;
|
|
46
|
+
* - between 3 and 4, the new `generated/` sits beside the old `AvClient.ts`: the
|
|
47
|
+
* validated `AvClient.ts` is still staged in `.aventara-ready-*`, so it is
|
|
48
|
+
* moved in, completing the new pair rather than leaving a mismatched one.
|
|
49
|
+
*
|
|
50
|
+
* Staging directories that never reached step 1 were not validated and are
|
|
51
|
+
* removed.
|
|
52
|
+
*
|
|
53
|
+
* # Failure is a sentence
|
|
54
|
+
*
|
|
55
|
+
* A refusal, a rejected tree or a filesystem error (an `Error` with a `code`) is
|
|
56
|
+
* an `OutputWriteError` naming its stage — the person running the generator can
|
|
57
|
+
* act on it (M3). Any other error is a defect and propagates with its stack —
|
|
58
|
+
* the very error thrown, not a wrapper — and what the run said before it is
|
|
59
|
+
* kept beside it, read by `warningsRaisedBeforeDefect`.
|
|
60
|
+
*
|
|
61
|
+
* A refusal carries every warning the run raised before it stopped, in the order
|
|
62
|
+
* a success would have returned them: a crash recovered in `prepare` changed the
|
|
63
|
+
* disk, and the person has to hear that even when the run then fails.
|
|
64
|
+
*
|
|
65
|
+
* A failed run removes the directories it created to reach `generateAt` — and
|
|
66
|
+
* only those, and only while empty — so a first run that fails leaves no empty
|
|
67
|
+
* `generateAt` behind.
|
|
68
|
+
*/
|
|
69
|
+
/** The four stages a write passes through, in order. */
|
|
70
|
+
export type OutputWriteStage = "prepare" | "temp" | "validate" | "replace";
|
|
71
|
+
/** A write that stopped; the previous output is intact unless it says otherwise. */
|
|
72
|
+
export declare class OutputWriteError extends Error {
|
|
73
|
+
readonly stage: OutputWriteStage;
|
|
74
|
+
readonly name = "OutputWriteError";
|
|
75
|
+
/**
|
|
76
|
+
* What the run said before it stopped, in the order a success returns its
|
|
77
|
+
* warnings: the emission's, the validator's, the writer's. Printed before the
|
|
78
|
+
* refusal's sentence.
|
|
79
|
+
*/
|
|
80
|
+
readonly warnings: readonly string[];
|
|
81
|
+
constructor(stage: OutputWriteStage, message: string, options?: {
|
|
82
|
+
readonly cause?: unknown;
|
|
83
|
+
readonly warnings?: readonly string[];
|
|
84
|
+
});
|
|
85
|
+
/** The same refusal, with `earlier` said before the warnings it already carries. */
|
|
86
|
+
precededBy(earlier: readonly string[]): OutputWriteError;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Records `earlier` as said before `defect`, ahead of anything already recorded,
|
|
90
|
+
* and returns `defect` unchanged so the caller rethrows the same error.
|
|
91
|
+
*/
|
|
92
|
+
export declare function precedeDefect(defect: unknown, earlier: readonly string[]): unknown;
|
|
93
|
+
/**
|
|
94
|
+
* What the run said before `defect` stopped it, in the order a success returns
|
|
95
|
+
* its warnings; empty when nothing was, or for an error no run threw.
|
|
96
|
+
*/
|
|
97
|
+
export declare function warningsRaisedBeforeDefect(defect: unknown): readonly string[];
|
|
98
|
+
/**
|
|
99
|
+
* The filesystem calls the writer makes, `node:fs/promises` by default. A seam
|
|
100
|
+
* because the filesystem is the writer's one external collaborator, and the only
|
|
101
|
+
* way to prove a failure at the replace stage leaves the previous output intact
|
|
102
|
+
* is to make a real rename fail.
|
|
103
|
+
*/
|
|
104
|
+
export type OutputFileSystem = Pick<typeof fsPromises, "lstat" | "mkdir" | "open" | "readdir" | "rename" | "rm" | "rmdir" | "writeFile">;
|
|
105
|
+
export interface ClientOutputWriteInput {
|
|
106
|
+
/** What `emitClientTree` returned: the tree, and the warnings it raised. */
|
|
107
|
+
readonly emission: ClientEmission;
|
|
108
|
+
/** The resolved `generateAt` (§15.2, architect 2026-10-04). Created when missing. */
|
|
109
|
+
readonly generateAt: AbsolutePath;
|
|
110
|
+
/**
|
|
111
|
+
* Content in `AvClient.ts` or `generated/` the generator did not produce, which
|
|
112
|
+
* the person running it confirmed may be overwritten or removed (architect,
|
|
113
|
+
* 2026-10-04) — the paths `findForeignOutputContent` listed. Anything foreign
|
|
114
|
+
* that is not listed here is refused.
|
|
115
|
+
*/
|
|
116
|
+
readonly overrideForeign?: readonly string[];
|
|
117
|
+
/** The `typescript` optional peer; the installed one by default (Q6). */
|
|
118
|
+
readonly resolveTypeScript?: TypeScriptResolver;
|
|
119
|
+
readonly fileSystem?: OutputFileSystem;
|
|
120
|
+
}
|
|
121
|
+
export interface ClientOutputWritten {
|
|
122
|
+
readonly generateAt: AbsolutePath;
|
|
123
|
+
/** How deeply the tree was validated before it replaced the output (Q6). */
|
|
124
|
+
readonly checked: OutputCheck;
|
|
125
|
+
/**
|
|
126
|
+
* Everything the run has to say without failing, in order: the emission's own
|
|
127
|
+
* warnings (the rename ladder's), then the validator's (a degraded check), then
|
|
128
|
+
* the writer's (a recovered crash, a leftover it could not remove). Returned,
|
|
129
|
+
* never printed: the CLI owns the terminal.
|
|
130
|
+
*/
|
|
131
|
+
readonly warnings: readonly string[];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Writes `emission.tree` as `<generateAt>/AvClient.ts` and `<generateAt>/generated/`.
|
|
135
|
+
*
|
|
136
|
+
* @throws OutputWriteError when the write stops; the previous output is intact.
|
|
137
|
+
*/
|
|
138
|
+
export declare function writeClientOutput(input: ClientOutputWriteInput): Promise<ClientOutputWritten>;
|
|
139
|
+
/**
|
|
140
|
+
* Lists what in `<generateAt>/AvClient.ts` and `<generateAt>/generated/` the
|
|
141
|
+
* generator did not produce — a file without the ownership line, a symbolic link,
|
|
142
|
+
* anything that is not a regular file — so the person running it can be asked
|
|
143
|
+
* before a generation overwrites or removes it (architect, 2026-10-04). Reads
|
|
144
|
+
* only; looks at nothing else in `generateAt`. Empty when `generateAt` does not
|
|
145
|
+
* exist yet.
|
|
146
|
+
*
|
|
147
|
+
* It sees what the write will see once it has recovered a killed run, without
|
|
148
|
+
* recovering it: a `generated/` the killed run moved aside with nothing in its
|
|
149
|
+
* place is read where it lies and its files named where recovery puts them back
|
|
150
|
+
* (`generated/…`). So the question asked, and `--yes`, cover them too — and a
|
|
151
|
+
* run that stops here has still touched nothing.
|
|
152
|
+
*
|
|
153
|
+
* @throws OutputWriteError when `generateAt` is not a directory, `generated` is
|
|
154
|
+
* not a directory or `AvClient.ts` is not a file: no confirmation repairs that.
|
|
155
|
+
*/
|
|
156
|
+
export declare function findForeignOutputContent(generateAt: AbsolutePath, fileSystem?: OutputFileSystem): Promise<readonly string[]>;
|
|
157
|
+
/**
|
|
158
|
+
* Whether `AvClient.ts` and `generated/` in `generateAt` hold exactly `tree` —
|
|
159
|
+
* every file byte for byte, no other file, and nothing a killed run moved aside —
|
|
160
|
+
* so that writing `tree` would change nothing (Phase 12-rest Q6: "up to date").
|
|
161
|
+
*/
|
|
162
|
+
export declare function ownedOutputMatches(generateAt: AbsolutePath, tree: EmittedTree): Promise<boolean>;
|