@aventara/client 0.1.0-pilot.1 → 0.1.0-pilot.2
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/README.md +41 -7
- package/dist/avclient.bin.js +0 -10
- package/dist/cli/command.parser.d.ts +15 -10
- package/dist/cli/command.parser.js +13 -19
- package/dist/cli/generate.command.js +0 -6
- package/dist/cli/generation-failure.renderer.js +0 -14
- package/dist/cli/generation-success.renderer.d.ts +4 -1
- package/dist/cli/generation-success.renderer.js +0 -13
- package/dist/cli/terminal.prompter.d.ts +1 -2
- package/dist/cli/warning.renderer.d.ts +2 -2
- package/dist/cli/warning.renderer.js +0 -8
- package/dist/cli.d.ts +8 -16
- package/dist/cli.js +5 -25
- package/dist/config/client-config.interface.d.ts +18 -16
- package/dist/config/client-config.interface.js +0 -13
- package/dist/config/config.loader.d.ts +34 -22
- package/dist/config/config.loader.js +49 -52
- package/dist/config/config.resolver.d.ts +13 -19
- package/dist/config/config.resolver.js +9 -48
- package/dist/config/env.cascade.d.ts +12 -14
- package/dist/config/env.cascade.js +0 -19
- package/dist/config/module-style.resolver.d.ts +52 -0
- package/dist/config/module-style.resolver.js +75 -0
- package/dist/config/tsconfig.locator.d.ts +45 -0
- package/dist/config/tsconfig.locator.js +52 -0
- package/dist/contract/contract.acceptance.d.ts +12 -26
- package/dist/contract/contract.acceptance.js +0 -54
- package/dist/contract/contract.fetcher.d.ts +12 -17
- package/dist/contract/contract.fetcher.js +0 -24
- package/dist/contract/contract.loader.d.ts +4 -5
- package/dist/contract/contract.loader.js +0 -10
- package/dist/emit/banner.emitter.d.ts +11 -12
- package/dist/emit/banner.emitter.js +0 -26
- package/dist/emit/client-surface.emitter.d.ts +17 -21
- package/dist/emit/client-surface.emitter.js +29 -55
- package/dist/emit/client-tree.emitter.d.ts +11 -20
- package/dist/emit/client-tree.emitter.js +12 -54
- package/dist/emit/contract-carrier.emitter.d.ts +5 -6
- package/dist/emit/contract-carrier.emitter.js +0 -28
- package/dist/emit/derivation.emitter.d.ts +7 -7
- package/dist/emit/derivation.emitter.js +2 -161
- package/dist/emit/descriptor.emitter.js +2 -28
- package/dist/emit/emitted-tree.interface.d.ts +40 -17
- package/dist/emit/emitted-tree.interface.js +6 -16
- package/dist/emit/enum.emitter.d.ts +4 -4
- package/dist/emit/enum.emitter.js +0 -24
- package/dist/emit/module-specifier.scanner.d.ts +25 -0
- package/dist/emit/module-specifier.scanner.js +160 -0
- package/dist/emit/module-style.interface.d.ts +58 -0
- package/dist/emit/module-style.interface.js +8 -0
- package/dist/emit/name.deriver.d.ts +33 -61
- package/dist/emit/name.deriver.js +0 -134
- package/dist/emit/named-type.emitter.d.ts +14 -21
- package/dist/emit/named-type.emitter.js +3 -30
- package/dist/emit/runtime.emitter.d.ts +23 -50
- package/dist/emit/runtime.emitter.js +68 -159
- package/dist/emit/scalar.codec.d.ts +20 -33
- package/dist/emit/scalar.codec.js +13 -69
- package/dist/emit/transaction.emitter.d.ts +6 -14
- package/dist/emit/transaction.emitter.js +24 -33
- package/dist/generate.d.ts +17 -34
- package/dist/generate.js +14 -22
- package/dist/index.js +0 -5
- package/dist/init/client-config.template.d.ts +6 -4
- package/dist/init/client-config.template.js +10 -13
- package/dist/init/client-init.errors.js +0 -3
- package/dist/init/client-init.orchestrator.js +1 -9
- package/dist/init/client-init.planner.d.ts +1 -9
- package/dist/init/client-init.planner.js +6 -24
- package/dist/init/client-init.questions.d.ts +8 -12
- package/dist/init/client-init.questions.js +0 -11
- package/dist/init/client-project.inspector.d.ts +6 -0
- package/dist/init/client-project.inspector.js +2 -2
- package/dist/node-version.guard.js +0 -12
- package/dist/output/output.validator.d.ts +22 -22
- package/dist/output/output.validator.js +46 -59
- package/dist/output/output.writer.d.ts +56 -52
- package/dist/output/output.writer.js +71 -133
- package/package.json +6 -4
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* R4, R5, Q16 rows 12–15 — `avclient init`'s questions in one table: each
|
|
3
|
-
* question's flag, label and default, from which the command line, the prompt,
|
|
4
|
-
* `--yes`'s defaults and the non-interactive refusal are derived — the rule
|
|
5
|
-
* `@aventara/cli`'s wizard follows, implemented here because the two packages
|
|
6
|
-
* never import each other (P7).
|
|
7
|
-
*/
|
|
8
1
|
export declare const PACKAGE_MANAGERS: readonly ["npm", "pnpm"];
|
|
9
2
|
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
10
3
|
export type ClientInitAnswers = {
|
|
11
|
-
/** The deployment's entrypoint
|
|
4
|
+
/** The deployment's entrypoint. */
|
|
12
5
|
readonly entrypoint: string;
|
|
13
|
-
/** The variable `framework.client.ts` reads it from, or `undefined` for a literal
|
|
6
|
+
/** The variable `framework.client.ts` reads it from, or `undefined` for a literal. */
|
|
14
7
|
readonly envVar: string | undefined;
|
|
15
|
-
/** Where the client is generated
|
|
8
|
+
/** Where the client is generated. */
|
|
16
9
|
readonly generateAt: string;
|
|
17
10
|
readonly packageManager: PackageManager;
|
|
18
11
|
};
|
|
@@ -34,7 +27,7 @@ export type ClientInitQuestion = {
|
|
|
34
27
|
/** @throws ClientInitAnswerError */
|
|
35
28
|
readonly parse: (raw: string) => string;
|
|
36
29
|
};
|
|
37
|
-
/** The project convention for the variable
|
|
30
|
+
/** The project convention for the variable. */
|
|
38
31
|
export declare const DEFAULT_ENV_VAR = "AVENTARA_API_URL";
|
|
39
32
|
export declare const CLIENT_INIT_QUESTIONS: readonly ClientInitQuestion[];
|
|
40
33
|
export type ClientInitSources = {
|
|
@@ -47,6 +40,9 @@ export type ClientInitSources = {
|
|
|
47
40
|
readonly say: (line: string) => void;
|
|
48
41
|
readonly detectedPackageManager: PackageManager;
|
|
49
42
|
};
|
|
50
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Flag → `--yes`'s default → the prompt → the refusal naming exactly the
|
|
45
|
+
* unanswered flags.
|
|
46
|
+
*/
|
|
51
47
|
export declare function resolveClientInitAnswers(sources: ClientInitSources): Promise<ClientInitAnswers>;
|
|
52
48
|
export {};
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { resolveEntrypoint } from "../config/config.resolver.js";
|
|
2
|
-
/**
|
|
3
|
-
* R4, R5, Q16 rows 12–15 — `avclient init`'s questions in one table: each
|
|
4
|
-
* question's flag, label and default, from which the command line, the prompt,
|
|
5
|
-
* `--yes`'s defaults and the non-interactive refusal are derived — the rule
|
|
6
|
-
* `@aventara/cli`'s wizard follows, implemented here because the two packages
|
|
7
|
-
* never import each other (P7).
|
|
8
|
-
*/
|
|
9
2
|
export const PACKAGE_MANAGERS = ["npm", "pnpm"];
|
|
10
|
-
/** An answer the question cannot take. A refusal: one sentence. */
|
|
11
3
|
export class ClientInitAnswerError extends Error {
|
|
12
4
|
name = "ClientInitAnswerError";
|
|
13
5
|
}
|
|
14
|
-
/** Nobody can answer and some questions are unanswered. A refusal: one sentence. */
|
|
15
6
|
export class ClientInitUnansweredError extends Error {
|
|
16
7
|
name = "ClientInitUnansweredError";
|
|
17
8
|
}
|
|
18
|
-
/** The project convention for the variable (spec §15.2). */
|
|
19
9
|
export const DEFAULT_ENV_VAR = "AVENTARA_API_URL";
|
|
20
10
|
export const CLIENT_INIT_QUESTIONS = [
|
|
21
11
|
{
|
|
@@ -77,7 +67,6 @@ export const CLIENT_INIT_QUESTIONS = [
|
|
|
77
67
|
},
|
|
78
68
|
},
|
|
79
69
|
];
|
|
80
|
-
/** Flag → `--yes`'s default → the prompt → the refusal naming exactly the unanswered flags (R5). */
|
|
81
70
|
export async function resolveClientInitAnswers(sources) {
|
|
82
71
|
const answered = {};
|
|
83
72
|
const missing = [];
|
|
@@ -7,6 +7,12 @@ export declare class ClientProjectRefusedError extends Error {
|
|
|
7
7
|
export type ClientProject = {
|
|
8
8
|
readonly directory: string;
|
|
9
9
|
readonly manifestText: string;
|
|
10
|
+
/**
|
|
11
|
+
* Its `framework.client.<ext>`, by name, when it has one — `undefined` when it
|
|
12
|
+
* has none. Two or more are refused while inspecting, as `avclient generate`
|
|
13
|
+
* refuses them.
|
|
14
|
+
*/
|
|
15
|
+
readonly configFile: string | undefined;
|
|
10
16
|
/** The package manager its lockfile names, if exactly one does. */
|
|
11
17
|
readonly lockfile: PackageManager | undefined;
|
|
12
18
|
/** The current text of a project file, or `undefined`. */
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
/** The directory is not a project `avclient init` can set up. A refusal: nothing written. */
|
|
3
|
+
import { findClientConfigFile } from "../config/config.loader.js";
|
|
5
4
|
export class ClientProjectRefusedError extends Error {
|
|
6
5
|
name = "ClientProjectRefusedError";
|
|
7
6
|
}
|
|
@@ -19,6 +18,7 @@ export function inspectClientProject(directory) {
|
|
|
19
18
|
return {
|
|
20
19
|
directory,
|
|
21
20
|
manifestText: readFileSync(manifest, "utf8"),
|
|
21
|
+
configFile: findClientConfigFile(directory),
|
|
22
22
|
lockfile: lockfiles.length === 1 ? lockfiles[0] : undefined,
|
|
23
23
|
read: (file) => {
|
|
24
24
|
try {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// biome-ignore lint/style/useNodejsImportProtocol: Node 14.0–14.13.0 resolves no "node:" specifier in an ES module, and this module runs on the Nodes the packages do not support.
|
|
2
1
|
import { readFileSync } from "fs";
|
|
3
2
|
function versionOf(text) {
|
|
4
3
|
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
@@ -16,11 +15,6 @@ function compareVersions(left, right) {
|
|
|
16
15
|
}
|
|
17
16
|
return 0;
|
|
18
17
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Whether `range` admits `version`. Reads `^x.y.z` (the same major, from the
|
|
21
|
-
* floor; majors ≥ 1) and `>=x.y.z`, joined by `||`; anything else throws rather
|
|
22
|
-
* than admit or refuse by guess.
|
|
23
|
-
*/
|
|
24
18
|
function admits(range, version) {
|
|
25
19
|
return range.split("||").some((part) => {
|
|
26
20
|
const comparator = /^\s*(\^|>=)(\d+\.\d+\.\d+)\s*$/.exec(part);
|
|
@@ -32,17 +26,11 @@ function admits(range, version) {
|
|
|
32
26
|
(comparator[1] === ">=" || version[0] === floor[0]));
|
|
33
27
|
});
|
|
34
28
|
}
|
|
35
|
-
/** The sentence a bin answers on `version`, or `undefined` when `range` admits it. */
|
|
36
29
|
export function nodeVersionRefusal(bin, packageName, range, version) {
|
|
37
30
|
return admits(range, versionOf(version))
|
|
38
31
|
? undefined
|
|
39
32
|
: `${bin}: Node ${version} is not supported; ${packageName} needs Node ${range}.`;
|
|
40
33
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Reads the manifest at `manifestUrl`; on a Node its `engines.node` does not
|
|
43
|
-
* admit, writes the sentence to stderr, sets exit code 1 and answers `true` —
|
|
44
|
-
* the bin then loads nothing else.
|
|
45
|
-
*/
|
|
46
34
|
export function refuseUnsupportedNode(bin, manifestUrl) {
|
|
47
35
|
const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
|
|
48
36
|
const range = manifest.engines === undefined ? undefined : manifest.engines.node;
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import type * as ts from "typescript";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ClientEmission } from "../emit/emitted-tree.interface.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
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.
|
|
4
|
+
* It judges a directory the writer has just filled with `tree`, and answers with a
|
|
5
|
+
* value — the writer decides what a rejection means for the previous output.
|
|
8
6
|
*
|
|
9
7
|
* # Three checks, the strongest available first
|
|
10
8
|
*
|
|
11
9
|
* - **Shape**, always: the directory holds exactly `tree` — no file missing, none
|
|
12
|
-
* extra, every one byte-for-byte what was emitted —
|
|
13
|
-
* ownership line
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* (
|
|
10
|
+
* extra, every one byte-for-byte what was emitted — every file carries the
|
|
11
|
+
* ownership line, and every module a file names is a file of the tree, spelled
|
|
12
|
+
* the project's way. The ownership line is not decoration: the NEXT run reads it
|
|
13
|
+
* to decide that this directory is a previous generation it may replace whole,
|
|
14
|
+
* so a file without it would make the next run refuse. The imports are read
|
|
15
|
+
* lexically (`module-specifier.scanner.ts`), with no compiler — so a wrongly
|
|
16
|
+
* spelled import is refused even where the type check cannot run (TypeScript 7).
|
|
17
|
+
* - **Types**, when the `typescript` optional peer resolves: a real program over
|
|
18
|
+
* the tree under a consumer's strictest plausible settings, in the project's
|
|
19
|
+
* module resolution and format (`compilerOptionsFor`). The tree is judged ALONE
|
|
20
|
+
* — the compiler host serves nothing outside the directory but TypeScript's own
|
|
21
|
+
* `lib` files, so an import the tree cannot satisfy itself fails here even when
|
|
22
|
+
* a `node_modules` beside the output could satisfy it.
|
|
22
23
|
* - **Syntax**, when it does not — or when the `typescript` that resolves has no
|
|
23
|
-
* classic compiler API
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* when their project compiles.
|
|
24
|
+
* classic compiler API: each file through Node's own TypeScript parser
|
|
25
|
+
* (`node:module`'s `stripTypeScriptTypes`), with a loud warning that the output
|
|
26
|
+
* was NOT type-checked. Degraded, never skipped, and never a refusal: the
|
|
27
|
+
* developer's own `tsc` still checks the tree when their project compiles.
|
|
28
28
|
*/
|
|
29
29
|
/** The `typescript` module, as the validator uses it. */
|
|
30
30
|
export type TypeScriptCompiler = typeof ts;
|
|
@@ -39,7 +39,7 @@ export type TypeScriptResolver = () => Promise<TypeScriptCompiler | undefined>;
|
|
|
39
39
|
* it up: an optional peer is linked beside the package that declares it.
|
|
40
40
|
*/
|
|
41
41
|
export declare function typeScriptResolverFor(specifier: string): TypeScriptResolver;
|
|
42
|
-
/** The `typescript` this package's optional peer names
|
|
42
|
+
/** The `typescript` this package's optional peer names. */
|
|
43
43
|
export declare const resolveInstalledTypeScript: TypeScriptResolver;
|
|
44
44
|
/** How deeply the tree was judged. */
|
|
45
45
|
export type OutputCheck = "types" | "syntax" | "shape";
|
|
@@ -72,4 +72,4 @@ export declare function typeScriptWithoutCompilerApiWarning(version: string, che
|
|
|
72
72
|
/**
|
|
73
73
|
* Judges `directory`, which the caller has just filled with `tree`.
|
|
74
74
|
*/
|
|
75
|
-
export declare function validateOutputTree(directory: string,
|
|
75
|
+
export declare function validateOutputTree(directory: string, emission: Pick<ClientEmission, "style" | "tree">, resolveTypeScript?: TypeScriptResolver): Promise<OutputValidation>;
|
|
@@ -4,10 +4,7 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
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
|
-
*/
|
|
7
|
+
import { scanModuleSpecifiers } from "../emit/module-specifier.scanner.js";
|
|
11
8
|
export function typeScriptResolverFor(specifier) {
|
|
12
9
|
return async () => {
|
|
13
10
|
let resolved;
|
|
@@ -24,58 +21,35 @@ export function typeScriptResolverFor(specifier) {
|
|
|
24
21
|
return loaded.default ?? loaded;
|
|
25
22
|
};
|
|
26
23
|
}
|
|
27
|
-
/** The `typescript` this package's optional peer names (Q5). */
|
|
28
24
|
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
25
|
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
26
|
return (typeof compiler.createProgram ===
|
|
40
27
|
"function");
|
|
41
28
|
}
|
|
42
|
-
/** The version a resolved `typescript` names, whatever its API. */
|
|
43
29
|
function versionOf(compiler) {
|
|
44
30
|
const { version } = compiler;
|
|
45
31
|
return typeof version === "string" ? version : "(no version)";
|
|
46
32
|
}
|
|
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
33
|
export const TYPESCRIPT_UNRESOLVED_WARNING = "`typescript` could not be resolved, so the generated output was NOT type-checked — " +
|
|
52
34
|
"only its shape and syntax were. Install `typescript` (an optional peer of @aventara/client) " +
|
|
53
35
|
"in this project to restore the type check.";
|
|
54
|
-
/** The warning a run with neither `typescript` nor Node's parser carries. */
|
|
55
36
|
export const SYNTAX_CHECK_UNAVAILABLE_WARNING = "neither `typescript` nor Node's TypeScript parser is available, so the generated " +
|
|
56
37
|
"output was NOT type-checked or parsed — only its shape was. Install `typescript` (an " +
|
|
57
38
|
"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
39
|
export function typeScriptWithoutCompilerApiWarning(version, checked) {
|
|
64
40
|
return (`the installed \`typescript\` ${version} has no classic compiler API (\`createProgram\`), so the generated ` +
|
|
65
41
|
`output was NOT type-checked${checked === "shape" ? " or parsed — only its shape was" : " — only its shape and syntax were"}. ` +
|
|
66
42
|
"Your project's own `tsc` checks it when it compiles; a `typescript` 5.5 to 6 restores the generator's own type check.");
|
|
67
43
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export async function validateOutputTree(directory, tree, resolveTypeScript = resolveInstalledTypeScript) {
|
|
72
|
-
const shapeFindings = await shapeFindingsOf(directory, tree);
|
|
44
|
+
export async function validateOutputTree(directory, emission, resolveTypeScript = resolveInstalledTypeScript) {
|
|
45
|
+
const { tree, style } = emission;
|
|
46
|
+
const shapeFindings = await shapeFindingsOf(directory, tree, style);
|
|
73
47
|
if (shapeFindings.length > 0) {
|
|
74
48
|
return { accepted: false, checked: "shape", findings: shapeFindings };
|
|
75
49
|
}
|
|
76
50
|
const compiler = await resolveTypeScript();
|
|
77
51
|
if (compiler !== undefined && hasClassicCompilerApi(compiler)) {
|
|
78
|
-
const findings = typeFindingsOf(compiler, directory, tree);
|
|
52
|
+
const findings = typeFindingsOf(compiler, directory, tree, style);
|
|
79
53
|
return findings.length > 0
|
|
80
54
|
? { accepted: false, checked: "types", findings }
|
|
81
55
|
: { accepted: true, checked: "types", warnings: [] };
|
|
@@ -105,10 +79,7 @@ export async function validateOutputTree(directory, tree, resolveTypeScript = re
|
|
|
105
79
|
],
|
|
106
80
|
};
|
|
107
81
|
}
|
|
108
|
-
|
|
109
|
-
* Shape *
|
|
110
|
-
* ------------------------------------------------------------------ */
|
|
111
|
-
async function shapeFindingsOf(directory, tree) {
|
|
82
|
+
async function shapeFindingsOf(directory, tree, style) {
|
|
112
83
|
const findings = [];
|
|
113
84
|
const expected = new Map(tree.map((file) => [file.path, file.bytes]));
|
|
114
85
|
const entries = await readdir(directory, {
|
|
@@ -151,29 +122,50 @@ async function shapeFindingsOf(directory, tree) {
|
|
|
151
122
|
if (!carriesGeneratedOwnership(text)) {
|
|
152
123
|
findings.push(`${relative}: does not carry the generated banner`);
|
|
153
124
|
}
|
|
125
|
+
for (const specifier of scanModuleSpecifiers(text).specifiers) {
|
|
126
|
+
if (!namesTreeFile(relative, specifier, style, expected)) {
|
|
127
|
+
findings.push(`${relative}: imports ${JSON.stringify(specifier)}, which is not a file of the emitted tree spelled with ${JSON.stringify(style.importFileExtension)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
154
130
|
}
|
|
155
131
|
return findings.sort();
|
|
156
132
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
133
|
+
function namesTreeFile(file, specifier, style, tree) {
|
|
134
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const target = path.posix.normalize(path.posix.join(path.posix.dirname(file), specifier));
|
|
138
|
+
const extension = file.startsWith("generated/derivation/")
|
|
139
|
+
? ".js"
|
|
140
|
+
: style.importFileExtension === ""
|
|
141
|
+
? ""
|
|
142
|
+
: `.${style.importFileExtension}`;
|
|
143
|
+
const spelled = extension === ""
|
|
144
|
+
? !/\.[cm]?[jt]s$/.test(target)
|
|
145
|
+
: target.endsWith(extension);
|
|
146
|
+
if (!spelled) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const stem = target.slice(0, target.length - extension.length);
|
|
150
|
+
return tree.has(`${stem}.ts`) || tree.has(`${stem}.d.ts`);
|
|
151
|
+
}
|
|
152
|
+
function compilerOptionsFor(compiler, style) {
|
|
153
|
+
const bundler = style.importFileExtension === "" && style.moduleFormat === "esm";
|
|
167
154
|
return {
|
|
168
155
|
target: compiler.ScriptTarget.ES2022,
|
|
169
|
-
module: compiler.ModuleKind.NodeNext,
|
|
170
|
-
moduleResolution:
|
|
156
|
+
module: bundler ? compiler.ModuleKind.ESNext : compiler.ModuleKind.NodeNext,
|
|
157
|
+
moduleResolution: bundler
|
|
158
|
+
? compiler.ModuleResolutionKind.Bundler
|
|
159
|
+
: compiler.ModuleResolutionKind.NodeNext,
|
|
160
|
+
...(style.importFileExtension === "ts"
|
|
161
|
+
? { allowImportingTsExtensions: true }
|
|
162
|
+
: {}),
|
|
171
163
|
lib: ["lib.es2022.d.ts"],
|
|
172
164
|
types: [],
|
|
173
165
|
strict: true,
|
|
174
166
|
noUncheckedIndexedAccess: true,
|
|
175
167
|
exactOptionalPropertyTypes: true,
|
|
176
|
-
verbatimModuleSyntax:
|
|
168
|
+
verbatimModuleSyntax: style.moduleFormat === "esm",
|
|
177
169
|
isolatedModules: true,
|
|
178
170
|
erasableSyntaxOnly: true,
|
|
179
171
|
noUnusedLocals: true,
|
|
@@ -186,15 +178,12 @@ function compilerOptionsFor(compiler) {
|
|
|
186
178
|
noEmit: true,
|
|
187
179
|
};
|
|
188
180
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
*/
|
|
194
|
-
const VIRTUAL_PACKAGE_JSON = '{ "type": "module" }';
|
|
195
|
-
function typeFindingsOf(compiler, directory, tree) {
|
|
181
|
+
function virtualPackageManifest(style) {
|
|
182
|
+
return `{ "type": "${style.moduleFormat === "esm" ? "module" : "commonjs"}" }`;
|
|
183
|
+
}
|
|
184
|
+
function typeFindingsOf(compiler, directory, tree, style) {
|
|
196
185
|
const root = path.resolve(directory);
|
|
197
|
-
const options = compilerOptionsFor(compiler);
|
|
186
|
+
const options = compilerOptionsFor(compiler, style);
|
|
198
187
|
const libraryDirectory = path.dirname(compiler.getDefaultLibFilePath(options));
|
|
199
188
|
const virtualPackageJson = path.join(root, "package.json");
|
|
200
189
|
const servable = (file) => isInside(root, file) || isInside(libraryDirectory, file);
|
|
@@ -205,7 +194,7 @@ function typeFindingsOf(compiler, directory, tree) {
|
|
|
205
194
|
host.fileExists = (file) => path.resolve(file) === virtualPackageJson ||
|
|
206
195
|
(servable(path.resolve(file)) && baseFileExists(file));
|
|
207
196
|
host.readFile = (file) => path.resolve(file) === virtualPackageJson
|
|
208
|
-
?
|
|
197
|
+
? virtualPackageManifest(style)
|
|
209
198
|
: servable(path.resolve(file))
|
|
210
199
|
? baseReadFile(file)
|
|
211
200
|
: undefined;
|
|
@@ -229,13 +218,11 @@ function typeFindingsOf(compiler, directory, tree) {
|
|
|
229
218
|
})
|
|
230
219
|
.trimEnd());
|
|
231
220
|
}
|
|
232
|
-
/** Whether `file` is `directory` or lies beneath it. */
|
|
233
221
|
function isInside(directory, file) {
|
|
234
222
|
const relative = path.relative(directory, file);
|
|
235
223
|
return (relative === "" ||
|
|
236
224
|
(!relative.startsWith("..") && !path.isAbsolute(relative)));
|
|
237
225
|
}
|
|
238
|
-
/** Node's TypeScript parser, where this Node has one. */
|
|
239
226
|
function nodeTypeScriptParser() {
|
|
240
227
|
const candidate = nodeModule.stripTypeScriptTypes;
|
|
241
228
|
return typeof candidate === "function" ? candidate : undefined;
|
|
@@ -3,29 +3,30 @@ import type { AbsolutePath } from "../config/client-config.interface.js";
|
|
|
3
3
|
import { type ClientEmission, type EmittedTree } from "../emit/emitted-tree.interface.js";
|
|
4
4
|
import { type OutputCheck, type TypeScriptResolver } from "./output.validator.js";
|
|
5
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
6
|
* The output is written into `generateAt`, a directory the developer SHARES: it
|
|
14
|
-
* may hold their own files. The generator owns exactly
|
|
15
|
-
* `AvClient.ts` and `generated/` — and reads, refuses on, moves or
|
|
16
|
-
* nothing else there, apart from the transient entries it names itself
|
|
17
|
-
* (`.aventara-next-*`, `.aventara-ready-*`, `.generated.aventara-previous`)
|
|
7
|
+
* may hold their own files. The generator owns exactly these entries in it — the
|
|
8
|
+
* entry file `AvClient.ts` and `generated/` — and reads, refuses on, moves or
|
|
9
|
+
* deletes nothing else there, apart from the transient entries it names itself
|
|
10
|
+
* (`.aventara-next-*`, `.aventara-ready-*`, `.generated.aventara-previous`) and an
|
|
11
|
+
* unreleased build's entry files, `AvClient.mjs` and `AvClient.d.mts`, which it
|
|
12
|
+
* removes only while they carry the ownership line.
|
|
18
13
|
*
|
|
19
14
|
* - `generated/` is replaced WHOLE, never merged: a file the previous run emitted
|
|
20
|
-
* and this one does not is gone
|
|
21
|
-
* carries the generated banner's
|
|
22
|
-
* nothing and is fine); otherwise the
|
|
23
|
-
*
|
|
15
|
+
* and this one does not is gone — an unreleased build's `.mjs` modules included.
|
|
16
|
+
* It is replaced only when every file in it carries the generated banner's
|
|
17
|
+
* ownership line (an empty directory owns nothing and is fine); otherwise the
|
|
18
|
+
* run is refused, naming the foreign files.
|
|
19
|
+
* - Each entry file is replaced only when it is absent or carries the ownership
|
|
24
20
|
* line; otherwise the run is refused, naming it.
|
|
21
|
+
* - `AvClient.mjs` and `AvClient.d.mts` — an unreleased build's entry — are
|
|
22
|
+
* removed when they carry the ownership line, so no stale JavaScript is left
|
|
23
|
+
* beside `AvClient.ts`. Without the line each is the developer's own file: never
|
|
24
|
+
* read beyond its head, never named, never removed. (pilot.0's and pilot.1's
|
|
25
|
+
* `AvClient.ts` is the entry file itself, replaced in place.)
|
|
25
26
|
*
|
|
26
27
|
* # The order, and its one window
|
|
27
28
|
*
|
|
28
|
-
* The whole tree —
|
|
29
|
+
* The whole tree — the entry files and `generated/` together — is written into a
|
|
29
30
|
* staging directory inside `generateAt` (same filesystem, so every move is one
|
|
30
31
|
* atomic `rename`) and validated there as one program. Then:
|
|
31
32
|
*
|
|
@@ -33,19 +34,22 @@ import { type OutputCheck, type TypeScriptResolver } from "./output.validator.js
|
|
|
33
34
|
* to be validated;
|
|
34
35
|
* 2. the previous `generated/` → `.generated.aventara-previous`;
|
|
35
36
|
* 3. the staged `generated/` → `generated/`;
|
|
36
|
-
* 4. the
|
|
37
|
+
* 4. the previous entry files, and an owned earlier entry, move aside into the
|
|
38
|
+
* ready directory; then each staged entry file moves in;
|
|
37
39
|
* 5. the previous `generated/` and the staging directory are removed.
|
|
38
40
|
*
|
|
39
|
-
* A failed step 3 renames the previous `generated/` straight back; a failed step
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* A failed step 3 renames the previous `generated/` straight back; a failed step 4
|
|
42
|
+
* moves what it moved back — the new entry files out, the previous ones in — then
|
|
43
|
+
* the new `generated/` out and the previous back, so a failure at any step leaves
|
|
44
|
+
* the previous output. Node has no atomic exchange of two entries, so two windows
|
|
45
|
+
* remain for a process KILLED mid-replace, and the next run's recovery closes each
|
|
46
|
+
* before it does anything else:
|
|
44
47
|
*
|
|
45
48
|
* - 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
|
|
47
|
-
* validated
|
|
48
|
-
* moved in, completing the new
|
|
49
|
+
* - between 3 and the end of 4, the new `generated/` sits beside the old entry
|
|
50
|
+
* files, or beside only some new ones: the validated entry files still staged in
|
|
51
|
+
* `.aventara-ready-*` are moved in, completing the new output rather than
|
|
52
|
+
* leaving a mismatched one.
|
|
49
53
|
*
|
|
50
54
|
* Staging directories that never reached step 1 were not validated and are
|
|
51
55
|
* removed.
|
|
@@ -54,16 +58,16 @@ import { type OutputCheck, type TypeScriptResolver } from "./output.validator.js
|
|
|
54
58
|
*
|
|
55
59
|
* A refusal, a rejected tree or a filesystem error (an `Error` with a `code`) is
|
|
56
60
|
* an `OutputWriteError` naming its stage — the person running the generator can
|
|
57
|
-
* act on it
|
|
58
|
-
*
|
|
59
|
-
*
|
|
61
|
+
* act on it. Any other error is a defect and propagates with its stack — the very
|
|
62
|
+
* error thrown, not a wrapper — and what the run said before it is kept beside it,
|
|
63
|
+
* read by `warningsRaisedBeforeDefect`.
|
|
60
64
|
*
|
|
61
|
-
* A refusal carries every warning the run raised before it stopped, in the order
|
|
62
|
-
*
|
|
65
|
+
* A refusal carries every warning the run raised before it stopped, in the order a
|
|
66
|
+
* success would have returned them: a crash recovered in `prepare` changed the
|
|
63
67
|
* disk, and the person has to hear that even when the run then fails.
|
|
64
68
|
*
|
|
65
|
-
* A failed run removes the directories it created to reach `generateAt` — and
|
|
66
|
-
*
|
|
69
|
+
* A failed run removes the directories it created to reach `generateAt` — and only
|
|
70
|
+
* those, and only while empty — so a first run that fails leaves no empty
|
|
67
71
|
* `generateAt` behind.
|
|
68
72
|
*/
|
|
69
73
|
/** The four stages a write passes through, in order. */
|
|
@@ -105,22 +109,22 @@ export type OutputFileSystem = Pick<typeof fsPromises, "lstat" | "mkdir" | "open
|
|
|
105
109
|
export interface ClientOutputWriteInput {
|
|
106
110
|
/** What `emitClientTree` returned: the tree, and the warnings it raised. */
|
|
107
111
|
readonly emission: ClientEmission;
|
|
108
|
-
/** The resolved `generateAt
|
|
112
|
+
/** The resolved `generateAt`. Created when missing. */
|
|
109
113
|
readonly generateAt: AbsolutePath;
|
|
110
114
|
/**
|
|
111
|
-
* Content in
|
|
112
|
-
* the person running it confirmed may be overwritten or removed
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
+
* Content in the entry files or `generated/` the generator did not produce, which
|
|
116
|
+
* the person running it confirmed may be overwritten or removed — the paths
|
|
117
|
+
* `findForeignOutputContent` listed. Anything foreign that is not listed here is
|
|
118
|
+
* refused.
|
|
115
119
|
*/
|
|
116
120
|
readonly overrideForeign?: readonly string[];
|
|
117
|
-
/** The `typescript` optional peer; the installed one by default
|
|
121
|
+
/** The `typescript` optional peer; the installed one by default. */
|
|
118
122
|
readonly resolveTypeScript?: TypeScriptResolver;
|
|
119
123
|
readonly fileSystem?: OutputFileSystem;
|
|
120
124
|
}
|
|
121
125
|
export interface ClientOutputWritten {
|
|
122
126
|
readonly generateAt: AbsolutePath;
|
|
123
|
-
/** How deeply the tree was validated before it replaced the output
|
|
127
|
+
/** How deeply the tree was validated before it replaced the output. */
|
|
124
128
|
readonly checked: OutputCheck;
|
|
125
129
|
/**
|
|
126
130
|
* Everything the run has to say without failing, in order: the emission's own
|
|
@@ -131,32 +135,32 @@ export interface ClientOutputWritten {
|
|
|
131
135
|
readonly warnings: readonly string[];
|
|
132
136
|
}
|
|
133
137
|
/**
|
|
134
|
-
* Writes `emission.tree` as `<generateAt>/AvClient.ts` and
|
|
138
|
+
* Writes `emission.tree` as `<generateAt>/AvClient.ts` and
|
|
139
|
+
* `<generateAt>/generated/`.
|
|
135
140
|
*
|
|
136
141
|
* @throws OutputWriteError when the write stops; the previous output is intact.
|
|
137
142
|
*/
|
|
138
143
|
export declare function writeClientOutput(input: ClientOutputWriteInput): Promise<ClientOutputWritten>;
|
|
139
144
|
/**
|
|
140
|
-
* Lists what in
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* exist yet.
|
|
145
|
+
* Lists what in the entry files and `<generateAt>/generated/` the generator did
|
|
146
|
+
* not produce — a file without the ownership line, a symbolic link, anything that
|
|
147
|
+
* is not a regular file — so the person running it can be asked before a
|
|
148
|
+
* generation overwrites or removes it. Reads only; looks at nothing else in
|
|
149
|
+
* `generateAt`. Empty when `generateAt` does not exist yet.
|
|
146
150
|
*
|
|
147
151
|
* It sees what the write will see once it has recovered a killed run, without
|
|
148
152
|
* recovering it: a `generated/` the killed run moved aside with nothing in its
|
|
149
153
|
* 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
|
-
*
|
|
154
|
+
* (`generated/…`). So the question asked, and `--yes`, cover them too — and a run
|
|
155
|
+
* that stops here has still touched nothing.
|
|
152
156
|
*
|
|
153
157
|
* @throws OutputWriteError when `generateAt` is not a directory, `generated` is
|
|
154
|
-
*
|
|
158
|
+
* not a directory or an entry file is not a file: no confirmation repairs that.
|
|
155
159
|
*/
|
|
156
160
|
export declare function findForeignOutputContent(generateAt: AbsolutePath, fileSystem?: OutputFileSystem): Promise<readonly string[]>;
|
|
157
161
|
/**
|
|
158
|
-
* Whether
|
|
159
|
-
* every file byte for byte, no other file,
|
|
160
|
-
* so that writing `tree` would change nothing
|
|
162
|
+
* Whether the entry files and `generated/` in `generateAt` hold exactly `tree` —
|
|
163
|
+
* every file byte for byte, no other file, no owned entry of an earlier build, and
|
|
164
|
+
* nothing a killed run moved aside — so that writing `tree` would change nothing.
|
|
161
165
|
*/
|
|
162
166
|
export declare function ownedOutputMatches(generateAt: AbsolutePath, tree: EmittedTree): Promise<boolean>;
|