@aventara/client 0.1.0-pilot.2 → 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 CHANGED
@@ -91,8 +91,9 @@ supported yet.
91
91
  - **A failed run leaves the previous output exactly as it was.** The whole tree is written to a staging directory inside
92
92
  `generateAt`, validated there as one program, and only then moved into place. A run killed part-way is repaired by the
93
93
  next run that writes, before it writes anything. A first run that fails removes the directories it created.
94
- - **The validate step is a real type check** when the optional peer `typescript` (5.5 to 6) is installed, under a
95
- consumer's strictest plausible settings and with nothing outside the tree resolvable. Without it — or under
94
+ - **The validate step is a real type check** when your project has the optional peer `typescript` (5.5 to 6) — it is
95
+ resolved from your project, so `npx @aventara/client@pilot init` type-checks too — under a consumer's strictest plausible
96
+ settings and with nothing outside the tree resolvable. Without it — or under
96
97
  TypeScript 7, which has no classic compiler API to check with — the check degrades to a parse with Node's own
97
98
  TypeScript parser, and says so loudly in one warning; it is never skipped, and the client is still written. The peer
98
99
  range has no upper bound, so a frontend on TypeScript 7 installs it; your own `tsc` checks the tree when it compiles.
@@ -32,7 +32,10 @@ export interface ClientGenerationInput {
32
32
  readonly overrideForeign?: boolean;
33
33
  /** Injected for tests; the platform `fetch` otherwise. */
34
34
  readonly fetch?: ContractFetch;
35
- /** The `typescript` optional peer; the installed one by default. */
35
+ /**
36
+ * The `typescript` optional peer; by default the project's own, resolved from
37
+ * `generateAt`.
38
+ */
36
39
  readonly resolveTypeScript?: TypeScriptResolver;
37
40
  }
38
41
  export interface ClientGenerated {
@@ -2,7 +2,9 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { runGenerate } from "../cli/generate.command.js";
4
4
  import { CLIENT_CONFIG_FILE } from "../config/config.loader.js";
5
+ import { resolveModuleStyle } from "../config/module-style.resolver.js";
5
6
  import { ContractTransportError } from "../contract/contract.fetcher.js";
7
+ import { CLIENT_ENTRY_FILE } from "../emit/emitted-tree.interface.js";
6
8
  import { ClientInitNotConfirmedError, ClientInitStepError, } from "./client-init.errors.js";
7
9
  import { GENERATE_SCRIPT, planClientInit } from "./client-init.planner.js";
8
10
  import { resolveClientInitAnswers } from "./client-init.questions.js";
@@ -26,6 +28,11 @@ export async function runClientInit(command, io) {
26
28
  detectedPackageManager: project.lockfile ??
27
29
  (userAgent?.split("/")[0] === "pnpm" ? "pnpm" : "npm"),
28
30
  });
31
+ const generateAt = path.resolve(io.cwd, answers.generateAt);
32
+ resolveModuleStyle({
33
+ generateAt,
34
+ entryFile: path.join(generateAt, CLIENT_ENTRY_FILE),
35
+ });
29
36
  const clientVersion = ownVersion();
30
37
  const plan = planClientInit({ project, answers, clientVersion });
31
38
  if (plan.conflicts.length > 0 && !command.yes) {
@@ -47,6 +47,16 @@ export function planClientInit(input) {
47
47
  replaced[at] = line;
48
48
  }
49
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
+ }
50
60
  }
51
61
  const manifest = (replace) => {
52
62
  const parsed = JSON.parse(project.manifestText);
@@ -14,17 +14,19 @@ import type { ClientEmission } from "../emit/emitted-tree.interface.js";
14
14
  * so a file without it would make the next run refuse. The imports are read
15
15
  * lexically (`module-specifier.scanner.ts`), with no compiler — so a wrongly
16
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.
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.
23
24
  * - **Syntax**, when it does not — or when the `typescript` that resolves has no
24
25
  * 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.
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. */
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, emission: Pick<ClientEmission, "style" | "tree">, resolveTypeScript?: TypeScriptResolver): Promise<OutputValidation>;
97
+ export declare function validateOutputTree(directory: string, emission: Pick<ClientEmission, "style" | "tree">, resolveTypeScript: TypeScriptResolver): Promise<OutputValidation>;
@@ -5,11 +5,11 @@ import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { carriesGeneratedOwnership } from "../emit/banner.emitter.js";
7
7
  import { scanModuleSpecifiers } from "../emit/module-specifier.scanner.js";
8
- export function typeScriptResolverFor(specifier) {
8
+ export function typeScriptResolverFor(specifier, directory) {
9
9
  return async () => {
10
10
  let resolved;
11
11
  try {
12
- resolved = createRequire(import.meta.url).resolve(specifier);
12
+ resolved = createRequire(path.join(directory, "package.json")).resolve(specifier);
13
13
  }
14
14
  catch (error) {
15
15
  if (isErrorWithCode(error, "MODULE_NOT_FOUND")) {
@@ -21,7 +21,25 @@ export function typeScriptResolverFor(specifier) {
21
21
  return loaded.default ?? loaded;
22
22
  };
23
23
  }
24
- export const resolveInstalledTypeScript = typeScriptResolverFor("typescript");
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
+ }
25
43
  function hasClassicCompilerApi(compiler) {
26
44
  return (typeof compiler.createProgram ===
27
45
  "function");
@@ -41,14 +59,34 @@ export function typeScriptWithoutCompilerApiWarning(version, checked) {
41
59
  `output was NOT type-checked${checked === "shape" ? " or parsed — only its shape was" : " — only its shape and syntax were"}. ` +
42
60
  "Your project's own `tsc` checks it when it compiles; a `typescript` 5.5 to 6 restores the generator's own type check.");
43
61
  }
44
- export async function validateOutputTree(directory, emission, resolveTypeScript = resolveInstalledTypeScript) {
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) {
45
82
  const { tree, style } = emission;
46
83
  const shapeFindings = await shapeFindingsOf(directory, tree, style);
47
84
  if (shapeFindings.length > 0) {
48
85
  return { accepted: false, checked: "shape", findings: shapeFindings };
49
86
  }
50
87
  const compiler = await resolveTypeScript();
51
- if (compiler !== undefined && hasClassicCompilerApi(compiler)) {
88
+ if (compiler !== undefined &&
89
+ degradedWarning(compiler, "syntax") === undefined) {
52
90
  const findings = typeFindingsOf(compiler, directory, tree, style);
53
91
  return findings.length > 0
54
92
  ? { accepted: false, checked: "types", findings }
@@ -59,11 +97,7 @@ export async function validateOutputTree(directory, emission, resolveTypeScript
59
97
  return {
60
98
  accepted: true,
61
99
  checked: "shape",
62
- warnings: [
63
- compiler === undefined
64
- ? SYNTAX_CHECK_UNAVAILABLE_WARNING
65
- : typeScriptWithoutCompilerApiWarning(versionOf(compiler), "shape"),
66
- ],
100
+ warnings: [degradedWarning(compiler, "shape")],
67
101
  };
68
102
  }
69
103
  const findings = syntaxFindingsOf(strip, tree);
@@ -72,11 +106,7 @@ export async function validateOutputTree(directory, emission, resolveTypeScript
72
106
  : {
73
107
  accepted: true,
74
108
  checked: "syntax",
75
- warnings: [
76
- compiler === undefined
77
- ? TYPESCRIPT_UNRESOLVED_WARNING
78
- : typeScriptWithoutCompilerApiWarning(versionOf(compiler), "syntax"),
79
- ],
109
+ warnings: [degradedWarning(compiler, "syntax")],
80
110
  };
81
111
  }
82
112
  async function shapeFindingsOf(directory, tree, style) {
@@ -225,7 +255,29 @@ function isInside(directory, file) {
225
255
  }
226
256
  function nodeTypeScriptParser() {
227
257
  const candidate = nodeModule.stripTypeScriptTypes;
228
- 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
+ };
229
281
  }
230
282
  function syntaxFindingsOf(strip, tree) {
231
283
  const decoder = new TextDecoder("utf-8");
@@ -118,7 +118,10 @@ export interface ClientOutputWriteInput {
118
118
  * refused.
119
119
  */
120
120
  readonly overrideForeign?: readonly string[];
121
- /** The `typescript` optional peer; the installed one by default. */
121
+ /**
122
+ * The `typescript` optional peer; by default the project's own, resolved from
123
+ * `generateAt`.
124
+ */
122
125
  readonly resolveTypeScript?: TypeScriptResolver;
123
126
  readonly fileSystem?: OutputFileSystem;
124
127
  }
@@ -3,7 +3,7 @@ import * as fsPromises from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { carriesGeneratedOwnership, GENERATED_OWNERSHIP_HEAD_BYTES, } from "../emit/banner.emitter.js";
5
5
  import { CLIENT_ENTRY_FILES, GENERATED_DIRECTORY, LEGACY_CLIENT_ENTRY_FILES, } from "../emit/emitted-tree.interface.js";
6
- import { resolveInstalledTypeScript, validateOutputTree, } from "./output.validator.js";
6
+ import { projectTypeScriptResolver, validateOutputTree, } from "./output.validator.js";
7
7
  export class OutputWriteError extends Error {
8
8
  stage;
9
9
  name = "OutputWriteError";
@@ -94,7 +94,7 @@ export async function writeClientOutput(input) {
94
94
  await fs.writeFile(target, file.bytes);
95
95
  }
96
96
  });
97
- const validation = await stage("validate", paths, () => validateOutputTree(staging, input.emission, input.resolveTypeScript ?? resolveInstalledTypeScript));
97
+ const validation = await stage("validate", paths, () => validateOutputTree(staging, input.emission, input.resolveTypeScript ?? projectTypeScriptResolver(generateAt)));
98
98
  if (validation.accepted) {
99
99
  validationWarnings = validation.warnings;
100
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/client",
3
- "version": "0.1.0-pilot.2",
3
+ "version": "0.1.0-pilot.3",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Development-time generator for Aventara typed remote clients.",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "LICENSE-ADDITIONAL-PERMISSION.md"
24
24
  ],
25
25
  "dependencies": {
26
- "@aventara/core": "0.1.0-pilot.2",
26
+ "@aventara/core": "0.1.0-pilot.3",
27
27
  "c12": "3.3.4",
28
28
  "get-tsconfig": "4.10.0"
29
29
  },
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "devDependencies": {
42
- "@aventara/testing": "0.1.0-pilot.2",
42
+ "@aventara/testing": "0.1.0-pilot.3",
43
43
  "@types/node": "24.10.1",
44
44
  "typescript": "^5.9.2"
45
45
  },