@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.
Files changed (79) hide show
  1. package/README.md +44 -9
  2. package/dist/avclient.bin.js +0 -10
  3. package/dist/cli/command.parser.d.ts +15 -10
  4. package/dist/cli/command.parser.js +13 -19
  5. package/dist/cli/generate.command.js +0 -6
  6. package/dist/cli/generation-failure.renderer.js +0 -14
  7. package/dist/cli/generation-success.renderer.d.ts +4 -1
  8. package/dist/cli/generation-success.renderer.js +0 -13
  9. package/dist/cli/terminal.prompter.d.ts +1 -2
  10. package/dist/cli/warning.renderer.d.ts +2 -2
  11. package/dist/cli/warning.renderer.js +0 -8
  12. package/dist/cli.d.ts +8 -16
  13. package/dist/cli.js +5 -25
  14. package/dist/config/client-config.interface.d.ts +18 -16
  15. package/dist/config/client-config.interface.js +0 -13
  16. package/dist/config/config.loader.d.ts +34 -22
  17. package/dist/config/config.loader.js +49 -52
  18. package/dist/config/config.resolver.d.ts +13 -19
  19. package/dist/config/config.resolver.js +9 -48
  20. package/dist/config/env.cascade.d.ts +12 -14
  21. package/dist/config/env.cascade.js +0 -19
  22. package/dist/config/module-style.resolver.d.ts +52 -0
  23. package/dist/config/module-style.resolver.js +75 -0
  24. package/dist/config/tsconfig.locator.d.ts +45 -0
  25. package/dist/config/tsconfig.locator.js +52 -0
  26. package/dist/contract/contract.acceptance.d.ts +12 -26
  27. package/dist/contract/contract.acceptance.js +0 -54
  28. package/dist/contract/contract.fetcher.d.ts +12 -17
  29. package/dist/contract/contract.fetcher.js +0 -24
  30. package/dist/contract/contract.loader.d.ts +4 -5
  31. package/dist/contract/contract.loader.js +0 -10
  32. package/dist/emit/banner.emitter.d.ts +11 -12
  33. package/dist/emit/banner.emitter.js +0 -26
  34. package/dist/emit/client-surface.emitter.d.ts +17 -21
  35. package/dist/emit/client-surface.emitter.js +29 -55
  36. package/dist/emit/client-tree.emitter.d.ts +11 -20
  37. package/dist/emit/client-tree.emitter.js +12 -54
  38. package/dist/emit/contract-carrier.emitter.d.ts +5 -6
  39. package/dist/emit/contract-carrier.emitter.js +0 -28
  40. package/dist/emit/derivation.emitter.d.ts +7 -7
  41. package/dist/emit/derivation.emitter.js +2 -161
  42. package/dist/emit/descriptor.emitter.js +2 -28
  43. package/dist/emit/emitted-tree.interface.d.ts +40 -17
  44. package/dist/emit/emitted-tree.interface.js +6 -16
  45. package/dist/emit/enum.emitter.d.ts +4 -4
  46. package/dist/emit/enum.emitter.js +0 -24
  47. package/dist/emit/module-specifier.scanner.d.ts +25 -0
  48. package/dist/emit/module-specifier.scanner.js +160 -0
  49. package/dist/emit/module-style.interface.d.ts +58 -0
  50. package/dist/emit/module-style.interface.js +8 -0
  51. package/dist/emit/name.deriver.d.ts +33 -61
  52. package/dist/emit/name.deriver.js +0 -134
  53. package/dist/emit/named-type.emitter.d.ts +14 -21
  54. package/dist/emit/named-type.emitter.js +3 -30
  55. package/dist/emit/runtime.emitter.d.ts +23 -50
  56. package/dist/emit/runtime.emitter.js +68 -159
  57. package/dist/emit/scalar.codec.d.ts +20 -33
  58. package/dist/emit/scalar.codec.js +13 -69
  59. package/dist/emit/transaction.emitter.d.ts +6 -14
  60. package/dist/emit/transaction.emitter.js +24 -33
  61. package/dist/generate.d.ts +20 -34
  62. package/dist/generate.js +14 -22
  63. package/dist/index.js +0 -5
  64. package/dist/init/client-config.template.d.ts +6 -4
  65. package/dist/init/client-config.template.js +10 -13
  66. package/dist/init/client-init.errors.js +0 -3
  67. package/dist/init/client-init.orchestrator.js +8 -9
  68. package/dist/init/client-init.planner.d.ts +1 -9
  69. package/dist/init/client-init.planner.js +16 -24
  70. package/dist/init/client-init.questions.d.ts +8 -12
  71. package/dist/init/client-init.questions.js +0 -11
  72. package/dist/init/client-project.inspector.d.ts +6 -0
  73. package/dist/init/client-project.inspector.js +2 -2
  74. package/dist/node-version.guard.js +0 -12
  75. package/dist/output/output.validator.d.ts +49 -27
  76. package/dist/output/output.validator.js +113 -74
  77. package/dist/output/output.writer.d.ts +59 -52
  78. package/dist/output/output.writer.js +72 -134
  79. package/package.json +6 -4
@@ -1,13 +1,5 @@
1
- import { CLIENT_CONFIG_FILE, LEGACY_CLIENT_CONFIG_FILE, } from "../config/config.loader.js";
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 config = clientConfigSource(answers);
33
- const existingConfig = project.read(CLIENT_CONFIG_FILE);
34
- // pilot.1: a `framework.client.ts` is moved to `framework.client.mts` — two
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(CLIENT_CONFIG_FILE);
28
+ conflicts.push(configFile);
47
29
  }
48
- add(CLIENT_CONFIG_FILE, existingConfig, existingConfig ?? config, config);
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 (Q16-12: `--entrypoint`, `defineClientConfig`'s key). */
4
+ /** The deployment's entrypoint. */
12
5
  readonly entrypoint: string;
13
- /** The variable `framework.client.ts` reads it from, or `undefined` for a literal (Q16-13). */
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 (Q16-14: `--generate-at`, the config key). */
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 (spec §15.2). */
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
- /** Flag → `--yes`'s default → the prompt → the refusal naming exactly the unanswered flags (R5). */
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
- /** The frontend `avclient init` serves, read before anything is asked or written. */
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 { EmittedTree } from "../emit/emitted-tree.interface.js";
2
+ import type { ClientEmission } from "../emit/emitted-tree.interface.js";
3
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.
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 — 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).
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 (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.
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 this package the way Node would look
39
- * it up: an optional peer is linked beside the package that declares it.
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
- /** The `typescript` this package's optional peer names (Q5). */
43
- export declare const resolveInstalledTypeScript: TypeScriptResolver;
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
- * Judges `directory`, which the caller has just filled with `tree`.
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, tree: EmittedTree, resolveTypeScript?: TypeScriptResolver): Promise<OutputValidation>;
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
- * 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) {
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(import.meta.url).resolve(specifier);
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
- /** 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
- */
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
- * 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);
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 && hasClassicCompilerApi(compiler)) {
78
- const findings = typeFindingsOf(compiler, directory, tree);
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
- * 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) {
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: compiler.ModuleResolutionKind.NodeNext,
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: true,
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
- * 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) {
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
- ? VIRTUAL_PACKAGE_JSON
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
- return typeof candidate === "function" ? candidate : undefined;
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");