@aventara/client 0.1.0-pilot.1 → 0.1.0-pilot.3
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 +44 -9
- 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 +20 -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 +8 -9
- package/dist/init/client-init.planner.d.ts +1 -9
- package/dist/init/client-init.planner.js +16 -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 +49 -27
- package/dist/output/output.validator.js +113 -74
- package/dist/output/output.writer.d.ts +59 -52
- package/dist/output/output.writer.js +72 -134
- package/package.json +6 -4
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import { CLIENT_CONFIG_FILE
|
|
1
|
+
import { CLIENT_CONFIG_FILE } from "../config/config.loader.js";
|
|
2
2
|
import { clientConfigSource } from "./client-config.template.js";
|
|
3
|
-
/**
|
|
4
|
-
* §4.5 — what `avclient init` writes, decided before anything is: the config
|
|
5
|
-
* file, the `.env` entry (only with a variable), the `avclient:generate` script
|
|
6
|
-
* and the exact devDependency (N6) — each computed with existing content kept
|
|
7
|
-
* and replaced, and every **conflict** (content it did not produce) named for
|
|
8
|
-
* the generateAt rule.
|
|
9
|
-
*/
|
|
10
|
-
/** R4: the script a frontend regenerates its client with. */
|
|
11
3
|
export const GENERATE_SCRIPT = "avclient:generate";
|
|
12
4
|
const ENV_LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/;
|
|
13
5
|
function unquoted(value) {
|
|
@@ -29,23 +21,13 @@ export function planClientInit(input) {
|
|
|
29
21
|
replacing.push({ path, content: replaced });
|
|
30
22
|
}
|
|
31
23
|
};
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
// configs would make `avclient generate` refuse. The one pilot.0 wrote for
|
|
36
|
-
// these answers moves without asking; anything else is a conflict.
|
|
37
|
-
const legacyConfig = project.read(LEGACY_CLIENT_CONFIG_FILE);
|
|
38
|
-
if (legacyConfig !== undefined) {
|
|
39
|
-
if (legacyConfig !== clientConfigSource(answers, LEGACY_CLIENT_CONFIG_FILE)) {
|
|
40
|
-
conflicts.push(`${LEGACY_CLIENT_CONFIG_FILE} (replaced by ${CLIENT_CONFIG_FILE})`);
|
|
41
|
-
}
|
|
42
|
-
keeping.push({ path: LEGACY_CLIENT_CONFIG_FILE, content: undefined });
|
|
43
|
-
replacing.push({ path: LEGACY_CLIENT_CONFIG_FILE, content: undefined });
|
|
44
|
-
}
|
|
24
|
+
const configFile = project.configFile ?? CLIENT_CONFIG_FILE;
|
|
25
|
+
const config = clientConfigSource(answers, configFile);
|
|
26
|
+
const existingConfig = project.read(configFile);
|
|
45
27
|
if (existingConfig !== undefined && existingConfig !== config) {
|
|
46
|
-
conflicts.push(
|
|
28
|
+
conflicts.push(configFile);
|
|
47
29
|
}
|
|
48
|
-
add(
|
|
30
|
+
add(configFile, existingConfig, existingConfig ?? config, config);
|
|
49
31
|
if (answers.envVar !== undefined) {
|
|
50
32
|
const before = project.read(".env");
|
|
51
33
|
const lines = before === undefined || before === ""
|
|
@@ -65,6 +47,16 @@ export function planClientInit(input) {
|
|
|
65
47
|
replaced[at] = line;
|
|
66
48
|
}
|
|
67
49
|
add(".env", before, `${kept.join("\n")}\n`, `${replaced.join("\n")}\n`);
|
|
50
|
+
const ignore = project.read(".gitignore");
|
|
51
|
+
const ignored = (ignore ?? "")
|
|
52
|
+
.split("\n")
|
|
53
|
+
.some((line) => line.trim() === ".env");
|
|
54
|
+
if (!ignored) {
|
|
55
|
+
const appended = ignore === undefined || ignore === ""
|
|
56
|
+
? ".env\n"
|
|
57
|
+
: `${ignore.endsWith("\n") ? ignore : `${ignore}\n`}.env\n`;
|
|
58
|
+
add(".gitignore", ignore, appended, appended);
|
|
59
|
+
}
|
|
68
60
|
}
|
|
69
61
|
const manifest = (replace) => {
|
|
70
62
|
const parsed = JSON.parse(project.manifestText);
|
|
@@ -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,32 @@
|
|
|
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 FROM THE PROJECT in
|
|
18
|
+
* its peer range: a real program over the tree under a consumer's strictest
|
|
19
|
+
* plausible settings, in the project's module resolution and format
|
|
20
|
+
* (`compilerOptionsFor`). The tree is judged ALONE — the compiler host serves
|
|
21
|
+
* nothing outside the directory but TypeScript's own `lib` files, so an import
|
|
22
|
+
* the tree cannot satisfy itself fails here even when a `node_modules` beside
|
|
23
|
+
* the output could satisfy it.
|
|
22
24
|
* - **Syntax**, when it does not — or when the `typescript` that resolves has no
|
|
23
|
-
* classic compiler API
|
|
24
|
-
*
|
|
25
|
-
* loud warning that the output was NOT type-checked. Degraded,
|
|
26
|
-
*
|
|
27
|
-
* when their project compiles.
|
|
25
|
+
* classic compiler API: each file through Node's own TypeScript parser
|
|
26
|
+
* (`node:module`'s `stripTypeScriptTypes`, its ExperimentalWarning held back),
|
|
27
|
+
* with a loud warning of our own that the output was NOT type-checked. Degraded,
|
|
28
|
+
* never skipped, and never a refusal: the developer's own `tsc` still checks the
|
|
29
|
+
* tree when their project compiles.
|
|
28
30
|
*/
|
|
29
31
|
/** The `typescript` module, as the validator uses it. */
|
|
30
32
|
export type TypeScriptCompiler = typeof ts;
|
|
@@ -35,12 +37,23 @@ export type TypeScriptCompiler = typeof ts;
|
|
|
35
37
|
*/
|
|
36
38
|
export type TypeScriptResolver = () => Promise<TypeScriptCompiler | undefined>;
|
|
37
39
|
/**
|
|
38
|
-
* A resolver for `specifier`, looked up from
|
|
39
|
-
*
|
|
40
|
+
* A resolver for `specifier`, looked up from `directory` the way Node looks up
|
|
41
|
+
* a package from a file there: its `node_modules`, then each parent's.
|
|
40
42
|
*/
|
|
41
|
-
export declare function typeScriptResolverFor(specifier: string): TypeScriptResolver;
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
export declare function typeScriptResolverFor(specifier: string, directory: string): TypeScriptResolver;
|
|
44
|
+
/**
|
|
45
|
+
* The `typescript` of the project that compiles the client, resolved from its
|
|
46
|
+
* `generateAt`. Never this package's own: run through `npx`, the generator is
|
|
47
|
+
* installed in npm's cache, where its optional peer never is — and the type check
|
|
48
|
+
* is a stand-in for the project's own `tsc`, so the project's compiler is the one
|
|
49
|
+
* that answers.
|
|
50
|
+
*/
|
|
51
|
+
export declare function projectTypeScriptResolver(generateAt: string): TypeScriptResolver;
|
|
52
|
+
/**
|
|
53
|
+
* The lowest `typescript` the generator type-checks with: this package's
|
|
54
|
+
* optional peer range, `>=5.5.0` (`packaging-gate.spec.ts` holds the two equal).
|
|
55
|
+
*/
|
|
56
|
+
export declare const MINIMUM_TYPESCRIPT = "5.5.0";
|
|
44
57
|
/** How deeply the tree was judged. */
|
|
45
58
|
export type OutputCheck = "types" | "syntax" | "shape";
|
|
46
59
|
export interface OutputAccepted {
|
|
@@ -70,6 +83,15 @@ export declare const SYNTAX_CHECK_UNAVAILABLE_WARNING: string;
|
|
|
70
83
|
*/
|
|
71
84
|
export declare function typeScriptWithoutCompilerApiWarning(version: string, checked: "syntax" | "shape"): string;
|
|
72
85
|
/**
|
|
73
|
-
*
|
|
86
|
+
* The warning a run carries when the project's `typescript` is older than the
|
|
87
|
+
* peer range: its compiler API lacks options the check sets (`Bundler`
|
|
88
|
+
* resolution, `verbatimModuleSyntax`), so its verdict would not be the one a
|
|
89
|
+
* supported compiler gives.
|
|
90
|
+
*/
|
|
91
|
+
export declare function typeScriptBelowRangeWarning(version: string, checked: "syntax" | "shape"): string;
|
|
92
|
+
/**
|
|
93
|
+
* Judges `directory`, which the caller has just filled with `tree`, with the
|
|
94
|
+
* `typescript` `resolveTypeScript` finds — the project's
|
|
95
|
+
* ({@link projectTypeScriptResolver}).
|
|
74
96
|
*/
|
|
75
|
-
export declare function validateOutputTree(directory: string,
|
|
97
|
+
export declare function validateOutputTree(directory: string, emission: Pick<ClientEmission, "style" | "tree">, resolveTypeScript: TypeScriptResolver): Promise<OutputValidation>;
|
|
@@ -4,15 +4,12 @@ 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
|
-
|
|
9
|
-
* it up: an optional peer is linked beside the package that declares it.
|
|
10
|
-
*/
|
|
11
|
-
export function typeScriptResolverFor(specifier) {
|
|
7
|
+
import { scanModuleSpecifiers } from "../emit/module-specifier.scanner.js";
|
|
8
|
+
export function typeScriptResolverFor(specifier, directory) {
|
|
12
9
|
return async () => {
|
|
13
10
|
let resolved;
|
|
14
11
|
try {
|
|
15
|
-
resolved = createRequire(
|
|
12
|
+
resolved = createRequire(path.join(directory, "package.json")).resolve(specifier);
|
|
16
13
|
}
|
|
17
14
|
catch (error) {
|
|
18
15
|
if (isErrorWithCode(error, "MODULE_NOT_FOUND")) {
|
|
@@ -24,58 +21,73 @@ export function typeScriptResolverFor(specifier) {
|
|
|
24
21
|
return loaded.default ?? loaded;
|
|
25
22
|
};
|
|
26
23
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
export function projectTypeScriptResolver(generateAt) {
|
|
25
|
+
return typeScriptResolverFor("typescript", generateAt);
|
|
26
|
+
}
|
|
27
|
+
export const MINIMUM_TYPESCRIPT = "5.5.0";
|
|
28
|
+
function meetsMinimum(version) {
|
|
29
|
+
const parts = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
30
|
+
const minimum = MINIMUM_TYPESCRIPT.split(".").map(Number);
|
|
31
|
+
if (parts === null) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
for (let at = 0; at < 3; at += 1) {
|
|
35
|
+
const have = Number(parts[at + 1]);
|
|
36
|
+
const need = minimum[at];
|
|
37
|
+
if (have !== need) {
|
|
38
|
+
return have > need;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
36
43
|
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
44
|
return (typeof compiler.createProgram ===
|
|
40
45
|
"function");
|
|
41
46
|
}
|
|
42
|
-
/** The version a resolved `typescript` names, whatever its API. */
|
|
43
47
|
function versionOf(compiler) {
|
|
44
48
|
const { version } = compiler;
|
|
45
49
|
return typeof version === "string" ? version : "(no version)";
|
|
46
50
|
}
|
|
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
51
|
export const TYPESCRIPT_UNRESOLVED_WARNING = "`typescript` could not be resolved, so the generated output was NOT type-checked — " +
|
|
52
52
|
"only its shape and syntax were. Install `typescript` (an optional peer of @aventara/client) " +
|
|
53
53
|
"in this project to restore the type check.";
|
|
54
|
-
/** The warning a run with neither `typescript` nor Node's parser carries. */
|
|
55
54
|
export const SYNTAX_CHECK_UNAVAILABLE_WARNING = "neither `typescript` nor Node's TypeScript parser is available, so the generated " +
|
|
56
55
|
"output was NOT type-checked or parsed — only its shape was. Install `typescript` (an " +
|
|
57
56
|
"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
57
|
export function typeScriptWithoutCompilerApiWarning(version, checked) {
|
|
64
58
|
return (`the installed \`typescript\` ${version} has no classic compiler API (\`createProgram\`), so the generated ` +
|
|
65
59
|
`output was NOT type-checked${checked === "shape" ? " or parsed — only its shape was" : " — only its shape and syntax were"}. ` +
|
|
66
60
|
"Your project's own `tsc` checks it when it compiles; a `typescript` 5.5 to 6 restores the generator's own type check.");
|
|
67
61
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
export function typeScriptBelowRangeWarning(version, checked) {
|
|
63
|
+
return (`the installed \`typescript\` ${version} is older than ${MINIMUM_TYPESCRIPT}, the lowest @aventara/client supports, so the generated ` +
|
|
64
|
+
`output was NOT type-checked${checked === "shape" ? " or parsed — only its shape was" : " — only its shape and syntax were"}. ` +
|
|
65
|
+
`Upgrade the project's \`typescript\` to ${MINIMUM_TYPESCRIPT} or later to restore the type check.`);
|
|
66
|
+
}
|
|
67
|
+
function degradedWarning(compiler, checked) {
|
|
68
|
+
if (compiler === undefined) {
|
|
69
|
+
return checked === "syntax"
|
|
70
|
+
? TYPESCRIPT_UNRESOLVED_WARNING
|
|
71
|
+
: SYNTAX_CHECK_UNAVAILABLE_WARNING;
|
|
72
|
+
}
|
|
73
|
+
if (!hasClassicCompilerApi(compiler)) {
|
|
74
|
+
return typeScriptWithoutCompilerApiWarning(versionOf(compiler), checked);
|
|
75
|
+
}
|
|
76
|
+
if (!meetsMinimum(versionOf(compiler))) {
|
|
77
|
+
return typeScriptBelowRangeWarning(versionOf(compiler), checked);
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
export async function validateOutputTree(directory, emission, resolveTypeScript) {
|
|
82
|
+
const { tree, style } = emission;
|
|
83
|
+
const shapeFindings = await shapeFindingsOf(directory, tree, style);
|
|
73
84
|
if (shapeFindings.length > 0) {
|
|
74
85
|
return { accepted: false, checked: "shape", findings: shapeFindings };
|
|
75
86
|
}
|
|
76
87
|
const compiler = await resolveTypeScript();
|
|
77
|
-
if (compiler !== undefined &&
|
|
78
|
-
|
|
88
|
+
if (compiler !== undefined &&
|
|
89
|
+
degradedWarning(compiler, "syntax") === undefined) {
|
|
90
|
+
const findings = typeFindingsOf(compiler, directory, tree, style);
|
|
79
91
|
return findings.length > 0
|
|
80
92
|
? { accepted: false, checked: "types", findings }
|
|
81
93
|
: { accepted: true, checked: "types", warnings: [] };
|
|
@@ -85,11 +97,7 @@ export async function validateOutputTree(directory, tree, resolveTypeScript = re
|
|
|
85
97
|
return {
|
|
86
98
|
accepted: true,
|
|
87
99
|
checked: "shape",
|
|
88
|
-
warnings: [
|
|
89
|
-
compiler === undefined
|
|
90
|
-
? SYNTAX_CHECK_UNAVAILABLE_WARNING
|
|
91
|
-
: typeScriptWithoutCompilerApiWarning(versionOf(compiler), "shape"),
|
|
92
|
-
],
|
|
100
|
+
warnings: [degradedWarning(compiler, "shape")],
|
|
93
101
|
};
|
|
94
102
|
}
|
|
95
103
|
const findings = syntaxFindingsOf(strip, tree);
|
|
@@ -98,17 +106,10 @@ export async function validateOutputTree(directory, tree, resolveTypeScript = re
|
|
|
98
106
|
: {
|
|
99
107
|
accepted: true,
|
|
100
108
|
checked: "syntax",
|
|
101
|
-
warnings: [
|
|
102
|
-
compiler === undefined
|
|
103
|
-
? TYPESCRIPT_UNRESOLVED_WARNING
|
|
104
|
-
: typeScriptWithoutCompilerApiWarning(versionOf(compiler), "syntax"),
|
|
105
|
-
],
|
|
109
|
+
warnings: [degradedWarning(compiler, "syntax")],
|
|
106
110
|
};
|
|
107
111
|
}
|
|
108
|
-
|
|
109
|
-
* Shape *
|
|
110
|
-
* ------------------------------------------------------------------ */
|
|
111
|
-
async function shapeFindingsOf(directory, tree) {
|
|
112
|
+
async function shapeFindingsOf(directory, tree, style) {
|
|
112
113
|
const findings = [];
|
|
113
114
|
const expected = new Map(tree.map((file) => [file.path, file.bytes]));
|
|
114
115
|
const entries = await readdir(directory, {
|
|
@@ -151,29 +152,50 @@ async function shapeFindingsOf(directory, tree) {
|
|
|
151
152
|
if (!carriesGeneratedOwnership(text)) {
|
|
152
153
|
findings.push(`${relative}: does not carry the generated banner`);
|
|
153
154
|
}
|
|
155
|
+
for (const specifier of scanModuleSpecifiers(text).specifiers) {
|
|
156
|
+
if (!namesTreeFile(relative, specifier, style, expected)) {
|
|
157
|
+
findings.push(`${relative}: imports ${JSON.stringify(specifier)}, which is not a file of the emitted tree spelled with ${JSON.stringify(style.importFileExtension)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
154
160
|
}
|
|
155
161
|
return findings.sort();
|
|
156
162
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
function namesTreeFile(file, specifier, style, tree) {
|
|
164
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const target = path.posix.normalize(path.posix.join(path.posix.dirname(file), specifier));
|
|
168
|
+
const extension = file.startsWith("generated/derivation/")
|
|
169
|
+
? ".js"
|
|
170
|
+
: style.importFileExtension === ""
|
|
171
|
+
? ""
|
|
172
|
+
: `.${style.importFileExtension}`;
|
|
173
|
+
const spelled = extension === ""
|
|
174
|
+
? !/\.[cm]?[jt]s$/.test(target)
|
|
175
|
+
: target.endsWith(extension);
|
|
176
|
+
if (!spelled) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const stem = target.slice(0, target.length - extension.length);
|
|
180
|
+
return tree.has(`${stem}.ts`) || tree.has(`${stem}.d.ts`);
|
|
181
|
+
}
|
|
182
|
+
function compilerOptionsFor(compiler, style) {
|
|
183
|
+
const bundler = style.importFileExtension === "" && style.moduleFormat === "esm";
|
|
167
184
|
return {
|
|
168
185
|
target: compiler.ScriptTarget.ES2022,
|
|
169
|
-
module: compiler.ModuleKind.NodeNext,
|
|
170
|
-
moduleResolution:
|
|
186
|
+
module: bundler ? compiler.ModuleKind.ESNext : compiler.ModuleKind.NodeNext,
|
|
187
|
+
moduleResolution: bundler
|
|
188
|
+
? compiler.ModuleResolutionKind.Bundler
|
|
189
|
+
: compiler.ModuleResolutionKind.NodeNext,
|
|
190
|
+
...(style.importFileExtension === "ts"
|
|
191
|
+
? { allowImportingTsExtensions: true }
|
|
192
|
+
: {}),
|
|
171
193
|
lib: ["lib.es2022.d.ts"],
|
|
172
194
|
types: [],
|
|
173
195
|
strict: true,
|
|
174
196
|
noUncheckedIndexedAccess: true,
|
|
175
197
|
exactOptionalPropertyTypes: true,
|
|
176
|
-
verbatimModuleSyntax:
|
|
198
|
+
verbatimModuleSyntax: style.moduleFormat === "esm",
|
|
177
199
|
isolatedModules: true,
|
|
178
200
|
erasableSyntaxOnly: true,
|
|
179
201
|
noUnusedLocals: true,
|
|
@@ -186,15 +208,12 @@ function compilerOptionsFor(compiler) {
|
|
|
186
208
|
noEmit: true,
|
|
187
209
|
};
|
|
188
210
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
*/
|
|
194
|
-
const VIRTUAL_PACKAGE_JSON = '{ "type": "module" }';
|
|
195
|
-
function typeFindingsOf(compiler, directory, tree) {
|
|
211
|
+
function virtualPackageManifest(style) {
|
|
212
|
+
return `{ "type": "${style.moduleFormat === "esm" ? "module" : "commonjs"}" }`;
|
|
213
|
+
}
|
|
214
|
+
function typeFindingsOf(compiler, directory, tree, style) {
|
|
196
215
|
const root = path.resolve(directory);
|
|
197
|
-
const options = compilerOptionsFor(compiler);
|
|
216
|
+
const options = compilerOptionsFor(compiler, style);
|
|
198
217
|
const libraryDirectory = path.dirname(compiler.getDefaultLibFilePath(options));
|
|
199
218
|
const virtualPackageJson = path.join(root, "package.json");
|
|
200
219
|
const servable = (file) => isInside(root, file) || isInside(libraryDirectory, file);
|
|
@@ -205,7 +224,7 @@ function typeFindingsOf(compiler, directory, tree) {
|
|
|
205
224
|
host.fileExists = (file) => path.resolve(file) === virtualPackageJson ||
|
|
206
225
|
(servable(path.resolve(file)) && baseFileExists(file));
|
|
207
226
|
host.readFile = (file) => path.resolve(file) === virtualPackageJson
|
|
208
|
-
?
|
|
227
|
+
? virtualPackageManifest(style)
|
|
209
228
|
: servable(path.resolve(file))
|
|
210
229
|
? baseReadFile(file)
|
|
211
230
|
: undefined;
|
|
@@ -229,16 +248,36 @@ function typeFindingsOf(compiler, directory, tree) {
|
|
|
229
248
|
})
|
|
230
249
|
.trimEnd());
|
|
231
250
|
}
|
|
232
|
-
/** Whether `file` is `directory` or lies beneath it. */
|
|
233
251
|
function isInside(directory, file) {
|
|
234
252
|
const relative = path.relative(directory, file);
|
|
235
253
|
return (relative === "" ||
|
|
236
254
|
(!relative.startsWith("..") && !path.isAbsolute(relative)));
|
|
237
255
|
}
|
|
238
|
-
/** Node's TypeScript parser, where this Node has one. */
|
|
239
256
|
function nodeTypeScriptParser() {
|
|
240
257
|
const candidate = nodeModule.stripTypeScriptTypes;
|
|
241
|
-
|
|
258
|
+
if (typeof candidate !== "function") {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
return (source) => {
|
|
262
|
+
const emitWarning = process.emitWarning;
|
|
263
|
+
process.emitWarning = function (warning, ...rest) {
|
|
264
|
+
const [kind] = rest;
|
|
265
|
+
const type = typeof kind === "string"
|
|
266
|
+
? kind
|
|
267
|
+
: kind?.type;
|
|
268
|
+
if (type === "ExperimentalWarning" &&
|
|
269
|
+
String(warning).startsWith("stripTypeScriptTypes ")) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
emitWarning.call(this, warning, ...rest);
|
|
273
|
+
};
|
|
274
|
+
try {
|
|
275
|
+
return candidate(source);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
process.emitWarning = emitWarning;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
242
281
|
}
|
|
243
282
|
function syntaxFindingsOf(strip, tree) {
|
|
244
283
|
const decoder = new TextDecoder("utf-8");
|