@aventara/client 0.1.0-pilot.4 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +679 -0
  2. package/README.md +168 -132
  3. package/dist/cli/command.parser.d.ts +6 -4
  4. package/dist/cli/command.parser.js +22 -11
  5. package/dist/cli/generate.command.d.ts +6 -1
  6. package/dist/cli/generate.command.js +12 -1
  7. package/dist/cli/generation-failure.renderer.js +24 -24
  8. package/dist/cli/step.reporter.d.ts +31 -0
  9. package/dist/cli/step.reporter.js +87 -0
  10. package/dist/cli.js +2 -0
  11. package/dist/config/client-config.interface.d.ts +7 -7
  12. package/dist/config/config.loader.d.ts +4 -4
  13. package/dist/config/config.loader.js +7 -7
  14. package/dist/config/config.resolver.d.ts +7 -7
  15. package/dist/config/config.resolver.js +12 -12
  16. package/dist/config/env.cascade.d.ts +2 -2
  17. package/dist/config/env.cascade.js +4 -4
  18. package/dist/config/module-style.resolver.d.ts +1 -1
  19. package/dist/config/module-style.resolver.js +2 -2
  20. package/dist/config/tsconfig.locator.d.ts +1 -1
  21. package/dist/config/tsconfig.locator.js +2 -2
  22. package/dist/contract/contract.acceptance.d.ts +4 -4
  23. package/dist/contract/contract.acceptance.js +4 -4
  24. package/dist/contract/contract.fetcher.d.ts +3 -3
  25. package/dist/contract/contract.fetcher.js +6 -6
  26. package/dist/contract/contract.loader.d.ts +6 -6
  27. package/dist/contract/contract.loader.js +2 -2
  28. package/dist/emit/client-surface.emitter.d.ts +2 -2
  29. package/dist/emit/client-surface.emitter.js +77 -43
  30. package/dist/emit/client-tree.emitter.d.ts +3 -3
  31. package/dist/emit/client-tree.emitter.js +8 -5
  32. package/dist/emit/contract-carrier.emitter.d.ts +3 -3
  33. package/dist/emit/contract-carrier.emitter.js +1 -1
  34. package/dist/emit/derivation.emitter.js +1 -0
  35. package/dist/emit/descriptor.emitter.d.ts +2 -2
  36. package/dist/emit/descriptor.emitter.js +2 -2
  37. package/dist/emit/emitted-tree.interface.d.ts +1 -1
  38. package/dist/emit/enum.emitter.d.ts +2 -2
  39. package/dist/emit/field-check.emitter.d.ts +21 -0
  40. package/dist/emit/field-check.emitter.js +603 -0
  41. package/dist/emit/json-shape.emitter.d.ts +16 -0
  42. package/dist/emit/json-shape.emitter.js +88 -0
  43. package/dist/emit/name.deriver.d.ts +6 -6
  44. package/dist/emit/name.deriver.js +15 -15
  45. package/dist/emit/named-type.emitter.d.ts +2 -2
  46. package/dist/emit/runtime.emitter.d.ts +2 -2
  47. package/dist/emit/runtime.emitter.js +117 -112
  48. package/dist/emit/scalar.codec.d.ts +1 -1
  49. package/dist/emit/scalar.codec.js +78 -35
  50. package/dist/emit/transaction.emitter.js +131 -83
  51. package/dist/generate.d.ts +11 -3
  52. package/dist/generate.js +40 -26
  53. package/dist/index.d.ts +1 -1
  54. package/dist/init/client-init.errors.d.ts +4 -4
  55. package/dist/init/client-init.errors.js +4 -4
  56. package/dist/init/client-init.orchestrator.js +37 -22
  57. package/dist/init/client-init.questions.d.ts +5 -5
  58. package/dist/init/client-init.questions.js +10 -10
  59. package/dist/init/client-project.inspector.d.ts +2 -2
  60. package/dist/init/client-project.inspector.js +3 -3
  61. package/dist/init/command.runner.d.ts +2 -0
  62. package/dist/init/command.runner.js +4 -1
  63. package/dist/output/output.writer.d.ts +5 -5
  64. package/dist/output/output.writer.js +12 -12
  65. package/package.json +6 -5
package/README.md CHANGED
@@ -2,19 +2,19 @@
2
2
 
3
3
  The development-time generator for Aventara typed remote clients. It fetches a deployed framework's `ClientContract`
4
4
  from `<entrypoint>/_contract`, verifies it, and writes a standalone TypeScript client into your project. The generated
5
- client is the product: it imports nothing from this package, or from `@aventara/core`, at runtime.
5
+ client is what you ship: at runtime it imports nothing from this package or from `@aventara/core`.
6
6
 
7
- Every argument and result type in the generated client is core's own derivation — `@aventara/core`'s published
8
- declarations, copied into the tree and instantiated over the deployment's ClientContract — so the client agrees with the
9
- server by construction.
7
+ Every argument and result type in the generated client is core's own derivation: `@aventara/core`'s published
8
+ declarations, copied into the tree and instantiated over the deployment's ClientContract. The client's types therefore
9
+ match the server's.
10
10
 
11
11
  ## Setting up a frontend: `avclient init`
12
12
 
13
13
  ```bash
14
- npx @aventara/client@pilot init # in your frontend project; pnpm: pnpm dlx @aventara/client@pilot init
14
+ npx @aventara/client@rc init # in your frontend project; pnpm: pnpm dlx @aventara/client@rc init
15
15
  ```
16
16
 
17
- It asks — or takes from flags, or with `--yes` takes every default — four things, and writes the setup:
17
+ It asks four things and writes the setup. Each answer can also come from a flag, and `--yes` takes every default:
18
18
 
19
19
  | Flag | Question | Default |
20
20
  |---|---|---|
@@ -23,12 +23,16 @@ It asks — or takes from flags, or with `--yes` takes every default — four th
23
23
  | `--generate-at <dir>` | Where the client goes | `./src/api` |
24
24
  | `--package-manager <npm\|pnpm>` | | the lockfile's, else the launching one, else npm |
25
25
 
26
- It writes `framework.client.ts` (or reuses the config the project already has), the variable into `.env` (only with a variable), `"avclient:generate": "avclient
27
- generate"` into the scripts and `@aventara/client` as an **exact** devDependency at its own version; runs the install
28
- (`--skip-install` prints it instead); and generates the client right away (`--skip-generate` to skip; a server that does
29
- not answer leaves the files and names `avclient generate`). Existing content that differs is listed and replaced only
30
- when you confirm, or with `--yes`; where nobody can be asked, nothing is touched. The generated tree is meant to be
31
- committed: regenerating it needs a running server.
26
+ It writes `framework.client.ts` (or reuses the config the project already has), the variable into `.env` (only with a
27
+ variable), `"avclient:generate": "avclient generate"` into the scripts, and `@aventara/client` as an exact
28
+ devDependency at its own version. Then it runs the install (`--skip-install` prints it instead) and generates the
29
+ client right away (`--skip-generate` skips that). A server that does not answer leaves the files in place and names
30
+ `avclient generate`; `--verbose` shows the install's output as it runs.
31
+
32
+ Each step (the install, loading the config, fetching the ClientContract, emitting, checking and writing) is shown on
33
+ stderr while it runs: live on a terminal, and one plain line per step in a pipe, in CI or with `NO_COLOR`. Existing
34
+ content that differs is listed and replaced only when you confirm, or with `--yes`; where nobody can be asked, nothing
35
+ is touched. Commit the generated tree, because regenerating it needs a running server.
32
36
 
33
37
  ## Generating a client
34
38
 
@@ -50,19 +54,26 @@ and run:
50
54
  npx avclient generate [--yes]
51
55
  ```
52
56
 
53
- **The config file** is loaded the way Prisma 7 loads `prisma.config.ts`: `framework.client.ts` — or `.mts`, `.cts`,
54
- `.js`, `.mjs`, `.cjs` — in ES module or CommonJS syntax, whatever your `package.json`'s `"type"`. A
55
- `framework.client.mts` written by 0.1.0-pilot.1 keeps working, and `avclient init` reuses it. Two config files in one
56
- directory are refused, naming both.
57
-
58
- - **`entrypoint`** is the deployment's framework entrypoint: origin plus mount path, one absolute `http(s)` URL. It
59
- may not carry credentials — the platform's `fetch` refuses such a URL, and the entrypoint ships inside the client.
60
- - **`generateAt`** is a directory you may share with your own files. The generator owns exactly two entries in it —
61
- `AvClient.ts` and `generated/` — and never reads, moves or removes anything else there.
57
+ The config file is loaded the way Prisma 7 loads `prisma.config.ts`: `framework.client.ts` (or `.mts`, `.cts`, `.js`,
58
+ `.mjs`, `.cjs`), in ES module or CommonJS syntax, whatever your `package.json`'s `"type"`. A `framework.client.mts`
59
+ written by 0.1.0-pilot.1 keeps working, and `avclient init` reuses it. Two config files in one directory are refused,
60
+ naming both.
61
+
62
+ - `entrypoint` is the deployment's framework entrypoint: origin plus mount path, one absolute `http(s)` URL. It may
63
+ not carry credentials, because the platform's `fetch` refuses such a URL and the entrypoint ships inside the client.
64
+ - `generateAt` is a directory you may share with your own files. The generator owns exactly two entries in it,
65
+ `AvClient.ts` and `generated/`, and never reads, moves or removes anything else there.
66
+ - The `.env` cascade is read from the current directory before the config is evaluated, highest precedence first: the
67
+ process environment, `.env.<mode>.local`, `.env.<mode>`, `.env.local`, `.env`; `mode` is `NODE_ENV`, or
68
+ `development`. The config reads it through `env("NAME")`; `process.env` is never written.
69
+ - Content the generator did not produce in `AvClient.ts` or `generated/` is listed, and you are asked before it is
70
+ overwritten or removed. That includes what a killed run left in the `generated/` it moved aside. `--yes` answers yes.
71
+ Where nobody can answer (stdin is not a terminal, as in CI), the run is refused and nothing is touched. A cancelled
72
+ or refused run still prints every warning it raised.
62
73
 
63
74
  ### Your tsconfig decides how the client is written
64
75
 
65
- The client is TypeScript source that **your own toolchain compiles**, like your own files — `tsc`, Next.js (Turbopack or
76
+ The client is TypeScript source that your own toolchain compiles, like your own files: `tsc`, Next.js (Turbopack or
66
77
  webpack), Vite, `tsx`, Node's type stripping. Its modules import each other the way your project imports its files,
67
78
  read from your `tsconfig.json` (the nearest one above `generateAt`, `extends` included) by Prisma 7's rules:
68
79
 
@@ -70,7 +81,7 @@ read from your `tsconfig.json` (the nearest one above `generateAt`, `extends` in
70
81
  |---|---|---|
71
82
  | `allowImportingTsExtensions` or `rewriteRelativeImportExtensions` (Vite's react-ts template, Node's type stripping) | `./generated/client.ts` | `./api/AvClient.ts` |
72
83
  | `moduleResolution: "bundler"` (Next.js) or `module: "commonjs"` | `./generated/client` | `./api/AvClient` |
73
- | anything else — `module: "nodenext"`/`"node16"` (NestJS 12) | `./generated/client.js` | `./api/AvClient.js` (ES module) or `./api/AvClient` (CommonJS) |
84
+ | anything else, such as `module: "nodenext"`/`"node16"` (NestJS 12) | `./generated/client.js` | `./api/AvClient.js` (ES module) or `./api/AvClient` (CommonJS) |
74
85
 
75
86
  Under `nodenext`, the nearest `package.json`'s `"type"` decides whether the client is checked as an ES module or as
76
87
  CommonJS. Vite's `tsconfig.json`, which only lists `references`, defers to the referenced project that includes the
@@ -78,29 +89,25 @@ client (`tsconfig.app.json`). The validate step type-checks the client in your p
78
89
 
79
90
  A project with no `tsconfig.json` is refused: the generated client is TypeScript, and JavaScript projects are not
80
91
  supported yet.
81
- - **The `.env` cascade** is read from the current directory before the config is evaluated, highest precedence first:
82
- the process environment, `.env.<mode>.local`, `.env.<mode>`, `.env.local`, `.env`; `mode` is `NODE_ENV`, or
83
- `development`. The config reads it through `env("NAME")`; `process.env` is never written.
84
- - **Content the generator did not produce** in `AvClient.ts` or `generated/` is listed and you are asked before it is
85
- overwritten or removed — including what a killed run left in the `generated/` it moved aside. `--yes` answers yes.
86
- Where nobody can answer — stdin is not a terminal, as in CI — the run is refused and nothing is touched. A cancelled
87
- or refused run still prints every warning it raised.
88
92
 
89
93
  ### What a run guarantees
90
94
 
91
- - **A failed run leaves the previous output exactly as it was.** The whole tree is written to a staging directory inside
92
- `generateAt`, validated there as one program, and only then moved into place. A run killed part-way is repaired by the
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 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
97
- TypeScript 7, which has no classic compiler API to check with — the check degrades to a parse with Node's own
98
- TypeScript parser, and says so loudly in one warning; it is never skipped, and the client is still written. The peer
99
- range has no upper bound, so a frontend on TypeScript 7 installs it; your own `tsc` checks the tree when it compiles.
100
- - **Determinism.** Two runs over one ClientContract write the same bytes; deleting the client and regenerating it gives
101
- the same bytes. Nothing volatile — no timestamp, generator version, host or path — is emitted.
102
- - **Failure is a sentence.** A refusal is one line and exit code 1, with no stack; anything the run warned about before
103
- it stopped is printed first. A stack means a defect in the generator.
95
+ A failed run leaves the previous output exactly as it was. The whole tree is written to a staging directory inside
96
+ `generateAt`, validated there as one program, and only then moved into place. A run killed part-way is repaired by the
97
+ next run that writes, before it writes anything. A first run that fails removes the directories it created.
98
+
99
+ The validate step is a real type check when your project has the optional peer `typescript` (5.5 to 6). It is resolved
100
+ from your project, so `npx @aventara/client@rc init` type-checks too, under a consumer's strictest plausible settings
101
+ and with nothing outside the tree resolvable. Without it, or under TypeScript 7 (which has no classic compiler API to
102
+ check with), the check falls back to a parse with Node's own TypeScript parser and says so in one warning. It is never
103
+ skipped, and the client is still written. The peer range has no upper bound, so a frontend on TypeScript 7 installs
104
+ it; your own `tsc` checks the tree when it compiles.
105
+
106
+ Two runs over one ClientContract write the same bytes, and deleting the client and regenerating it gives the same
107
+ bytes. Nothing volatile (no timestamp, generator version, host or path) is emitted.
108
+
109
+ A refusal is one line and exit code 1, with no stack; anything the run warned about before it stopped is printed
110
+ first. A stack means a defect in the generator.
104
111
 
105
112
  ## Calling the API
106
113
 
@@ -113,22 +120,22 @@ const one: User = await avClient.User.find.unique({ where: { id } });
113
120
  const maybe = await avClient.User.find.first({ where }); // User | null — a miss is null
114
121
  ```
115
122
 
116
- - **`avClient`** is the default export: the client of the deployment the tree was generated from.
117
- `new AvClient({ entrypoint, fetch })` makes another — `entrypoint` for a deployment serving **exactly the same**
118
- ClientContract (tests, SSR), `fetch` for a custom fetch.
119
- - **One property per Resource**, one per family under it, one per advertised variant: only what the ClientContract
120
- advertises exists, in the type and at runtime. A Resource named like one of the client's own members (`tx`,
121
- `transaction`, `then`, `constructor`, `Object.prototype`'s names) is reached as `<name>Model`, with one warning; its
122
- wire name is unchanged.
123
- - **A call resolves to the data**: the record, a list, a count; a first-style miss (`A1001`) is `null`. Every other
124
- outcome throws — the `FrameworkError` subclass of its code (`NotFoundError` for a strict-unique miss,
125
- `ContractMismatchError` for a stale client, …), or `TransportError` when no framework answer arrived.
126
- - **Values are application values**: `bigint`, `Date`, `Uint8Array`, the client's `Decimal`, JSON values as they are.
127
- Arguments are encoded and results revived for you; a value that cannot be read throws `TransportError`.
128
- - **Per-call options** (`CallOptions`), never sent in the body: `signal` (an abort rejects with what `fetch` rejected
123
+ - `avClient` is the default export: the client of the deployment the tree was generated from.
124
+ `new AvClient({ entrypoint, fetch })` makes another. `entrypoint` is for a deployment serving exactly the same
125
+ ClientContract (tests, SSR), and `fetch` is for a custom fetch.
126
+ - The client has one property per Resource, one per family under it, and one per advertised variant. Only what the
127
+ ClientContract advertises exists, in the type and at runtime. A Resource named like one of the client's own members
128
+ (`tx`, `transaction`, `then`, `constructor`, `Object.prototype`'s names) is reached as `<name>Model`, with one
129
+ warning; its wire name is unchanged.
130
+ - A call resolves to the data: the record, a list, a count. A first-style miss (`A1001`) is `null`. Every other
131
+ outcome throws the `AvError` subclass of its code (`AvNotFoundError` for a strict-unique miss,
132
+ `AvContractMismatchError` for a stale client, …), or `AvTransportError` when no framework answer arrived.
133
+ - Values are application values: `bigint`, `Date`, `Uint8Array`, the client's `AvDecimal`, JSON values as they are.
134
+ Arguments are encoded and results revived for you; a value that cannot be read throws `AvTransportError`.
135
+ - Per-call options (`AvCallOptions`) are never sent in the body: `signal` (an abort rejects with what `fetch` rejected
129
136
  with), `headers` (the framework's own identity headers are refused), and `requestId`, sent as `Aventara-Request-Id`
130
137
  and winning over that header in `headers`.
131
- - **Named types** (`User`, `UserWhere`, `UserUniqueWhere`, `UserOrderBy`, `UserCreateData`, `UserUpdateData`,
138
+ - The named types (`User`, `UserWhere`, `UserUniqueWhere`, `UserOrderBy`, `UserCreateData`, `UserUpdateData`,
132
139
  `UserSelect`, `UserInclude`) are named exports, each only when the operation it reads is advertised. A name that would
133
140
  clash walks the rename ladder (`User` beside a Resource `UserWhere` becomes `UserModel`), one warning each.
134
141
 
@@ -143,17 +150,37 @@ const profile = avClient.tx.Profile.create.one({ data: { bio: "…", userId: cre
143
150
  const [user, row] = await avClient.transaction([created, profile]);
144
151
  ```
145
152
 
153
+ A `$ref` stands for one field of an earlier step's result. It may be written in a step's data at any depth (a field's
154
+ value, the key a relation's `$connect`, `$disconnect`, `$set`, `$connectOrCreate` or `$upsert` names, the `where` of a
155
+ nested `$update` or `$delete`, and the data `$create`, `$update` and `$upsert` nest), in its `cursor`, and anywhere in
156
+ its `where`, `AND`/`OR`/`NOT` and relation filters included. A projection's nested `where` and `orderBy` take none:
157
+
158
+ ```ts
159
+ const post = avClient.tx.Post.create.one({ data: { title: "Hi", author: { $connect: { id: created.$ref("id") } } } });
160
+ const mine = avClient.tx.Post.find.many({ where: { OR: [{ authorId: created.$ref("id") }, { title: { startsWith: created.$ref("email") } }] } });
161
+ ```
162
+
163
+ Its source field must have the same type as where it is written:
164
+
165
+ - for a filter's value, `equals` and `not`, a nullable source only where the field is nullable, and a `null` then
166
+ matches the rows where the field is unset, as a literal `null` does;
167
+ - for a unique key, a `cursor`, a comparison or a text operator, a non-null source;
168
+ - for `in`, `notIn`, `hasEvery` and `hasSome`, a list field;
169
+ - for `has`, one value.
170
+
171
+ A mismatch is a type error, and the server refuses it before anything runs (`A2007`/`V1011`).
172
+
146
173
  `avClient.tx.…` builds a handle and sends nothing. `avClient.transaction([...])` sends the plan once and resolves one
147
174
  result per handle, typed, in order; a failing step throws its error with `cause.operation` naming the step. `$ref` binds
148
175
  to the handle's position in the list it runs with. Two mistakes are refused before anything is sent, as core refuses
149
- them in process: one handle twice (`ValidationError`, `A2004`/`V1001`), and a `$ref` to a handle not in the list
150
- (`ValidationError`, `A2007`/`V1010`). A handle is plain data: any client of the same generated tree may run it.
176
+ them in process: one handle twice (`AvValidationError`, `A2004`/`V1001`), and a `$ref` to a handle not in the list
177
+ (`AvValidationError`, `A2007`/`V1010`). A handle is plain data: any client of the same generated tree may run it.
151
178
 
152
179
  ## Custom fetch and authentication
153
180
 
154
- `fetch` is typed as **your platform's own `fetch`** when your `lib` declares one (DOM, `@types/node`), so the platform
155
- `fetch` is accepted without a cast; with neither, a structural fetch type stands in. The client never adds headers of its
156
- own beyond the protocol's; authentication is a custom fetch:
181
+ `fetch` is typed as your platform's own `fetch` when your `lib` declares one (DOM, `@types/node`), so the platform
182
+ `fetch` is accepted without a cast; with neither, a structural fetch type stands in. The client adds no headers of its
183
+ own beyond the protocol's, so authentication goes in a custom fetch:
157
184
 
158
185
  ```ts
159
186
  const client = new AvClient({
@@ -161,107 +188,116 @@ const client = new AvClient({
161
188
  });
162
189
  ```
163
190
 
164
- The platform `fetch` is read when a call is made, not when the client is imported, so importing the client never fails
165
- where there is none — a call does, saying why. An entrypoint carrying credentials is refused when the config is
191
+ The platform `fetch` is read when a call is made, not when the client is imported. Importing the client never fails
192
+ where there is no `fetch`; a call does, saying why. An entrypoint carrying credentials is refused when the config is
166
193
  resolved.
167
194
 
168
195
  ## Up to date
169
196
 
170
- A rerun asks the deployment conditionally (`If-None-Match`, the ClientContract hash of the output it
171
- finds — only when that output is intact and its carrier re-verifies). On `304 Not Modified` it re-emits from that
172
- stored ClientContract with **this** generator and **this** entrypoint: identical bytes print
197
+ A rerun asks the deployment conditionally (`If-None-Match`, the ClientContract hash of the output it finds), but only
198
+ when that output is intact and its carrier re-verifies. On `304 Not Modified` it re-emits from that stored
199
+ ClientContract with this generator and this entrypoint. Identical bytes print
173
200
  `avclient: up to date: … nothing was written.` and exit 0; otherwise the output is replaced, and the line says
174
201
  `the ClientContract is unchanged; the generator or the entrypoint changed`.
175
202
 
176
203
  ## What is emitted
177
204
 
178
205
  Every file opens with a banner stating that it is generated and must not be edited, and with the Biome suppressions a
179
- generated file needs — emitted, not configured, so the output stays correct in a repository whose formatter nobody here
180
- chose. The `.d.ts` files are core's copied declarations under `generated/derivation/` and the named types in
181
- `generated/types.d.ts`; the `.ts` sources carry their own types.
182
-
183
- - `AvClient.ts` — the entry point you import: `avClient` (the default export), `AvClient`, `AvClientOptions`,
184
- `CallOptions`, `Fetch`, `Operation`, the named Resource types, the enums, `Decimal`, `FrameworkError` and its
185
- subclasses, `TransportError`, and the types `Cause`, `ValidationIssue`, `OperationCode` and `ValidationCode`.
186
- - `generated/client.ts` — the typed client: the class `AvClient`, `AvClientOptions`, `CallOptions` and the ready
187
- instance `avClient`; every argument and result type an instantiation of core's own derivation.
188
- - `generated/contract.ts` — the ClientContract the client was generated against: its served canonical bytes, verbatim,
206
+ generated file needs. They are emitted rather than configured, so the output stays correct whatever formatter the
207
+ repository uses. The `.d.ts` files are core's copied declarations under `generated/derivation/`
208
+ and the named types in `generated/types.d.ts`; the `.ts` sources carry their own types.
209
+
210
+ - `AvClient.ts`: the entry point you import: `avClient` (the default export), `AvClient`, `AvClientOptions`,
211
+ `AvCallOptions`, `AvFetch`, `AvOperation`, the named Resource types, the enums, `AvDecimal`, `AvError` and its
212
+ subclasses, `AvTransportError`, and the types `AvCause`, `AvValidationIssue`, `AvOperationCode` and `AvValidationCode`.
213
+ - `generated/client.ts`: the typed client: the class `AvClient`, `AvClientOptions`, `AvCallOptions` and the ready
214
+ instance `avClient`. Every argument and result type is an instantiation of core's own derivation.
215
+ - `generated/contract.ts`: the ClientContract the client was generated against, its served canonical bytes verbatim,
189
216
  as the type `ClientContractShape` (no runtime byte).
190
- - `generated/derivation/` — `@aventara/core`'s published declarations of the argument/result derivation and the
191
- call grammar, copied byte for byte (bannered, source-map trailer dropped): the generated types are core's own.
192
- - `generated/enums.ts` — each enum as a union type and a same-named `as const` object.
193
- - `generated/metadata.ts` — the ClientContract hash and protocol version the client is bound to, and the entrypoint it
194
- was generated from, its default deployment (an entrypoint carrying credentials is refused at resolution).
195
- - `generated/runtime/codec.ts` — the nine scalars' wire codecs, and the request-body serializer.
196
- - `generated/runtime/decimal.ts` — the framework's `Decimal`: a constructor, `toString` and `toJSON`.
197
- - `generated/runtime/descriptor.ts` — what the runtime reads of the contract: the result fields it revives and the
217
+ - `generated/derivation/`: `@aventara/core`'s published declarations of the argument/result derivation and the call
218
+ grammar, copied byte for byte (bannered, source-map trailer dropped), so the generated types are core's own.
219
+ - `generated/enums.ts`: each enum as a union type and a same-named `as const` object.
220
+ - `generated/metadata.ts`: the ClientContract hash and protocol version the client is bound to, and the entrypoint it
221
+ was generated from, which is its default deployment (an entrypoint carrying credentials is refused at resolution).
222
+ - `generated/runtime/codec.ts`: the nine scalars' wire codecs, and the request-body serializer.
223
+ - `generated/runtime/decimal.ts`: the framework's `AvDecimal`, with a constructor, `toString` and `toJSON`.
224
+ - `generated/runtime/descriptor.ts`: what the runtime reads of the contract: the result fields it revives and the
198
225
  relations it follows (the decode table), and the advertised operations.
199
- - `generated/runtime/errors.ts` — `FrameworkError`, its subclasses by code class (`AuthError`, `ConflictError`,
200
- `ContractMismatchError`, `InternalError`, `NotFoundError`, `ProtocolError`, `ValidationError`), `TransportError`, and
201
- the code unions, emitted from core's code arrays rather than retyped.
202
- - `generated/runtime/fingerprint.ts` — the operation fingerprint (RFC 8785 canonical JSON, SHA-256,
203
- base64url), dependency-free and pinned to core's; emitted only when transactions are `interactive`.
204
- - `generated/runtime/transaction.ts` — `avClient.tx`'s deferred handles and `avClient.transaction`'s plan assembly,
205
- refusals and one POST to `/_transactions`; emitted only when transactions are `interactive`.
206
- - `generated/runtime/transport.ts` — one POST per operation with the identity headers, and the response read by the
207
- protocol's rule; arguments encoded by value, results revived by the decode table, the platform's own `fetch` type accepted.
208
- Its `execute` primitive is module-private: no public file re-exports it; `AvClient`'s methods wrap it.
209
- - `generated/types.d.ts` — the named Resource types, as a declaration file (`User`, `UserWhere`,
210
- `UserUniqueWhere`, `UserOrderBy`, `UserCreateData`, `UserUpdateData`, `UserSelect`, `UserInclude`), each only when
211
- its operation is advertised.
212
-
213
- The tree is self-contained: every import in it names a file in it, so it compiles with nothing else installed. Names a TypeScript
214
- declaration cannot carry as-is (a reserved word, a name the runtime owns, an enum sharing a Resource's name) are
215
- renamed, with one warning each; the wire name is kept.
226
+ - `generated/runtime/errors.ts`: `AvError`, its subclasses by code class (`AvAuthError`, `AvConflictError`,
227
+ `AvContractMismatchError`, `AvInternalError`, `AvNotFoundError`, `AvProtocolError`, `AvValidationError`),
228
+ `AvTransportError`, and the code unions, emitted from core's code arrays instead of retyped.
229
+ - `generated/runtime/field-check.evaluator.ts`: core's own field-check evaluator, copied byte for byte with its
230
+ imports pointed at `generated/derivation/`. Emitted only when the ClientContract carries a check (a field's
231
+ `validators`, or a check on a Json Shape node).
232
+ - `generated/runtime/field-validators.ts`: the ClientContract's checks as data, and the walk that runs them on an
233
+ operation's arguments before anything is sent. A failure throws the server's `A2004` with the same issues. A check
234
+ the client cannot judge exactly as the server would is left to the server. Emitted only when the ClientContract
235
+ carries a check.
236
+ - `generated/runtime/fingerprint.ts`: the operation fingerprint (RFC 8785 canonical JSON, SHA-256, base64url),
237
+ dependency-free and pinned to core's. Emitted only when transactions are `interactive`.
238
+ - `generated/runtime/transaction.ts`: `avClient.tx`'s deferred handles and `avClient.transaction`'s plan assembly,
239
+ refusals and one POST to `/_transactions`. Emitted only when transactions are `interactive`.
240
+ - `generated/runtime/transport.ts`: one POST per operation with the identity headers, and the response read by the
241
+ protocol's rule. Arguments are encoded by value, results revived by the decode table, and the platform's own `fetch`
242
+ type is accepted. Its `execute` primitive is module-private: no public file re-exports it, and `AvClient`'s methods
243
+ wrap it.
244
+ - `generated/types.d.ts`: the named Resource types, as a declaration file (`User`, `UserWhere`, `UserUniqueWhere`,
245
+ `UserOrderBy`, `UserCreateData`, `UserUpdateData`, `UserSelect`, `UserInclude`), each only when its operation is
246
+ advertised.
247
+
248
+ The tree is self-contained: every import in it names a file in it, so it compiles with nothing else installed. Names a
249
+ TypeScript declaration cannot carry as-is (a reserved word, a name the runtime owns, an enum sharing a Resource's name)
250
+ are renamed, with one warning each; the wire name is kept.
216
251
 
217
252
  ## What a generated client is bound to
218
253
 
219
- A generated client is bound to the **exact ClientContract hash and protocol version** it was generated from. Both are
220
- in `generated/metadata.ts` and both are sent on every request (`Aventara-Protocol-Version`, `Aventara-Contract-Hash`).
221
- A deployment whose ClientContract differs refuses the request with `A2005`, which the client throws as
222
- `ContractMismatchError`.
254
+ A generated client is bound to the exact ClientContract hash and protocol version it was generated from. Both are in
255
+ `generated/metadata.ts`, and both are sent on every request (`Aventara-Protocol-Version`, `Aventara-Contract-Hash`). A
256
+ deployment whose ClientContract differs refuses the request with `A2005`, which the client throws as
257
+ `AvContractMismatchError`.
223
258
 
224
- The hash covers everything the deployment advertises, not only its schema: two deployments of one schema can advertise
259
+ The hash covers everything the deployment advertises, not only its schema. Two deployments of one schema can advertise
225
260
  different ClientContracts, and a client generated against one is refused by the other. Whatever the cause, the remedy
226
- is the same — **generate against the deployment the client will call, and regenerate when it changes.**
261
+ is the same: generate against the deployment the client will call, and regenerate when it changes.
227
262
 
228
- That holds for an operation the deployment **no longer advertises**, too: the server checks the client's
229
- identity before it routes, so a stale client calling a removed operation also hears `A2005` and throws
230
- `ContractMismatchError`. A response that is not a framework envelope at all — a host's own 404 for a path it does not
231
- mount — is a `TransportError`, which names no remedy: nothing in it says the client is stale.
263
+ The same holds for an operation the deployment no longer advertises. The server checks the client's identity before it
264
+ routes, so a stale client calling a removed operation also hears `A2005` and throws `AvContractMismatchError`. A
265
+ response that is not a framework envelope at all, such as a host's own 404 for a path it does not mount, is an
266
+ `AvTransportError`. It names no remedy, because nothing in it says the client is stale.
232
267
 
233
- The embedded entrypoint is a **default**: `new AvClient({ entrypoint })` may point a client at another deployment, and
234
- doing so re-binds nothing — the ClientContract hash still decides whether that deployment accepts the client.
268
+ The embedded entrypoint is a default. `new AvClient({ entrypoint })` may point a client at another deployment without
269
+ re-binding anything: the ClientContract hash still decides whether that deployment accepts the client.
235
270
 
236
271
  ## Type-checking cost in a consumer
237
272
 
238
- A consumer pays for the calls it type-checks, not for the schema, under `skipLibCheck: true` (the common default):
239
- the named types (`generated/types.d.ts`) and core's derivation are declaration files checked only where read. Under
273
+ Under `skipLibCheck: true` (the common default), a consumer pays for the calls it type-checks, not for the schema: the
274
+ named types (`generated/types.d.ts`) and core's derivation are declaration files checked only where read. Under
240
275
  `skipLibCheck: false` every named type is resolved where it is declared, so the cost grows with the number of
241
276
  Resources.
242
277
 
243
278
  ## Before 1.0
244
279
 
245
- - **The `Aventara` wire prefix** of the protocol's headers (`Aventara-Protocol-Version`, `Aventara-Contract-Hash`,
246
- `Aventara-Request-Id`) may still change before the first stable release. It is one constant in the generated
247
- `generated/runtime/transport.ts`; regenerating picks up a change.
280
+ The `Aventara` wire prefix of the protocol's headers (`Aventara-Protocol-Version`, `Aventara-Contract-Hash`,
281
+ `Aventara-Request-Id`) may still change before the first stable release. It is one constant in the generated
282
+ `generated/runtime/transport.ts`, and regenerating picks up a change.
248
283
 
249
284
  ## Known issues
250
285
 
251
- - **The generator read the wrong `tsconfig.json`.** In a monorepo or a non-standard layout, the nearest
252
- `tsconfig.json` above `generateAt` may not be the one your project compiles the client with, and the client's
253
- imports or module format then do not match your compiler (`Cannot find module './generated/client.js'`, an
254
- extension your bundler will not resolve). Name the right one in `framework.client.ts`, relative to the config file:
286
+ The generator can read the wrong `tsconfig.json`. In a monorepo or a non-standard layout, the nearest `tsconfig.json`
287
+ above `generateAt` may not be the one your project compiles the client with. The client's imports or module format then
288
+ do not match your compiler (`Cannot find module './generated/client.js'`, or an extension your bundler will not
289
+ resolve). Name the right one in `framework.client.ts`, relative to the config file:
255
290
 
256
- ```ts
257
- export default defineClientConfig({
258
- entrypoint: env("AVENTARA_API_URL"),
259
- generateAt: "./src/api",
260
- tsconfigFile: "./tsconfig.app.json",
261
- });
262
- ```
291
+ ```ts
292
+ export default defineClientConfig({
293
+ entrypoint: env("AVENTARA_API_URL"),
294
+ generateAt: "./src/api",
295
+ tsconfigFile: "./tsconfig.app.json",
296
+ });
297
+ ```
263
298
 
264
299
  ## License
265
300
 
266
- PolyForm Shield 1.0.0 with an additional permission — free to use, including in commercial applications; you may not
267
- use it to build a product that competes with Aventara itself. See `LICENSE` and `LICENSE-ADDITIONAL-PERMISSION.md`.
301
+ PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
302
+ may not use it to build a product that competes with Aventara itself. See `LICENSE` and
303
+ `LICENSE-ADDITIONAL-PERMISSION.md`.
@@ -1,6 +1,6 @@
1
1
  export declare const USAGE = "avclient \u2014 generate a typed Aventara client from a deployed ClientContract.\n\nUsage:\n avclient generate [--yes] Read framework.client.ts (or .js, .mjs, .cjs, .mts,\n .cts) in the current directory, fetch\n <entrypoint>/_contract, and write AvClient.ts and\n generated/ into its generateAt directory, its imports\n spelled as the project's tsconfig.json says.\n avclient init [options] Set up this frontend: write framework.client.ts, the\n .env entry, the avclient:generate script and the\n @aventara/client devDependency; install; generate.\n --entrypoint <url> The server's entrypoint [http://localhost:3000/api].\n --env-var <NAME> Read it from this variable [AVENTARA_API_URL].\n --no-env-var Write it into framework.client.ts as a literal.\n --generate-at <dir> Where the client goes [./src/api].\n --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]\n --skip-install Print the install instead of running it.\n --skip-generate Do not generate now.\n -y, --yes Accept every default, and replace differing content.\n avclient --help Print this and exit 0.\n avclient <command> --help Print that command's usage and exit 0.\n avclient --version, -v Print this generator's version and exit 0.\n\nOptions:\n -y, --yes Overwrite or remove content in AvClient.ts and generated/ that the\n generator did not produce, without asking. Without it, such content\n is listed and you are asked; where nobody can answer (stdin is not a\n terminal, as in CI), the run is refused and nothing is touched.\n\nThe generator owns AvClient.ts and generated/ in generateAt, and nothing else\nthere: your own files beside them are never read, moved or removed. Generated\nfiles are replaced on every run; do not edit them.\n\nBefore framework.client.ts is evaluated, the .env cascade is read from the\ncurrent directory, highest precedence first: the process environment,\n.env.<mode>.local, .env.<mode>, .env.local, .env \u2014 where mode is NODE_ENV, or\n\"development\".\n";
2
2
  export declare const GENERATE_USAGE = "Usage: avclient generate [--yes]\n\nRead framework.client.ts (or .js, .mjs, .cjs, .mts, .cts) in the current directory,\nfetch <entrypoint>/_contract, and write AvClient.ts and generated/ into its\ngenerateAt directory, its imports spelled as the project's tsconfig.json says.\n\nOptions:\n -y, --yes Overwrite or remove content in AvClient.ts and generated/ that the\n generator did not produce, without asking. Without it, such content\n is listed and you are asked; where nobody can answer (stdin is not a\n terminal, as in CI), the run is refused and nothing is touched.\n\nThe generator owns AvClient.ts and generated/ in generateAt, and nothing else\nthere: your own files beside them are never read, moved or removed.\n\nBefore the config is evaluated, the .env cascade is read from the current\ndirectory, highest precedence first: the process environment, .env.<mode>.local,\n.env.<mode>, .env.local, .env \u2014 where mode is NODE_ENV, or \"development\".\n";
3
- export declare const INIT_USAGE = "Usage: avclient init [options]\n\nSet up this frontend: write framework.client.ts, the .env entry, the avclient:generate\nscript and the @aventara/client devDependency; install; generate.\n\nOptions:\n --entrypoint <url> The server's entrypoint [http://localhost:3000/api].\n --env-var <NAME> Read it from this variable [AVENTARA_API_URL].\n --no-env-var Write it into framework.client.ts as a literal.\n --generate-at <dir> Where the client goes [./src/api].\n --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]\n --skip-install Print the install instead of running it.\n --skip-generate Do not generate now.\n -y, --yes Accept every default, and replace differing content.\n\nOn a terminal every unanswered question is asked; anywhere else, pass its flag\nor --yes, or the run stops before writing anything.\n";
3
+ export declare const INIT_USAGE = "Usage: avclient init [options]\n\nSet up this frontend: write framework.client.ts, the .env entry, the avclient:generate\nscript and the @aventara/client devDependency; install; generate.\n\nOptions:\n --entrypoint <url> The server's entrypoint [http://localhost:3000/api].\n --env-var <NAME> Read it from this variable [AVENTARA_API_URL].\n --no-env-var Write it into framework.client.ts as a literal.\n --generate-at <dir> Where the client goes [./src/api].\n --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]\n --skip-install Print the install instead of running it.\n --skip-generate Do not generate now.\n --verbose Show the install's output as it runs.\n -y, --yes Accept every default, and replace differing content.\n\nOn a terminal every unanswered question is asked; anywhere else, pass its flag\nor --yes, or the run stops before writing anything.\n";
4
4
  export type ClientInitCommand = {
5
5
  readonly command: "init";
6
6
  readonly given: Readonly<Partial<Record<"entrypoint" | "envVar" | "generateAt" | "packageManager", string>>>;
@@ -8,6 +8,8 @@ export type ClientInitCommand = {
8
8
  readonly skipInstall: boolean;
9
9
  readonly skipGenerate: boolean;
10
10
  readonly yes: boolean;
11
+ /** Streams the install's output. */
12
+ readonly verbose?: boolean;
11
13
  };
12
14
  /** What the command line asked for. */
13
15
  export type CliCommand = {
@@ -20,11 +22,11 @@ export type CliCommand = {
20
22
  readonly yes: boolean;
21
23
  } | ClientInitCommand;
22
24
  /** The command line cannot be understood. A refusal: one sentence, exit 1. */
23
- export declare class CliCommandError extends Error {
24
- readonly name = "CliCommandError";
25
+ export declare class AvCliCommandError extends Error {
26
+ readonly name = "AvCliCommandError";
25
27
  }
26
28
  /**
27
- * @throws CliCommandError when `argv` is not `generate [--yes|-y]`, `init …`,
29
+ * @throws AvCliCommandError when `argv` is not `generate [--yes|-y]`, `init …`,
28
30
  * `--help`/`-h` or `--version`/`-v`.
29
31
  */
30
32
  export declare function parseCliCommand(argv: readonly string[]): CliCommand;
@@ -69,19 +69,20 @@ Options:
69
69
  --package-manager <npm|pnpm> [the lockfile's, else the launching one, else npm]
70
70
  --skip-install Print the install instead of running it.
71
71
  --skip-generate Do not generate now.
72
+ --verbose Show the install's output as it runs.
72
73
  -y, --yes Accept every default, and replace differing content.
73
74
 
74
75
  On a terminal every unanswered question is asked; anywhere else, pass its flag
75
76
  or --yes, or the run stops before writing anything.
76
77
  `;
77
- export class CliCommandError extends Error {
78
- name = "CliCommandError";
78
+ export class AvCliCommandError extends Error {
79
+ name = "AvCliCommandError";
79
80
  }
80
81
  export function parseCliCommand(argv) {
81
82
  const [command, ...rest] = argv;
82
83
  const help = "run `avclient --help` for usage.";
83
84
  if (command === undefined) {
84
- throw new CliCommandError(`a command is required; ${help}`);
85
+ throw new AvCliCommandError(`a command is required; ${help}`);
85
86
  }
86
87
  if (command === "--help" || command === "-h") {
87
88
  return { command: "help" };
@@ -97,7 +98,7 @@ export function parseCliCommand(argv) {
97
98
  return parseInit(rest, help);
98
99
  }
99
100
  if (command !== "generate") {
100
- throw new CliCommandError(`unknown command ${JSON.stringify(command)}; ${help}`);
101
+ throw new AvCliCommandError(`unknown command ${JSON.stringify(command)}; ${help}`);
101
102
  }
102
103
  let yes = false;
103
104
  for (const argument of rest) {
@@ -105,7 +106,7 @@ export function parseCliCommand(argv) {
105
106
  yes = true;
106
107
  continue;
107
108
  }
108
- throw new CliCommandError(`unexpected argument ${JSON.stringify(argument)}: generate takes only --yes, ` +
109
+ throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)}: generate takes only --yes, ` +
109
110
  `everything else it needs is in ${CLIENT_CONFIG_FILE}; ${help}`);
110
111
  }
111
112
  return { command: "generate", yes };
@@ -132,24 +133,29 @@ function parseInit(argv, help) {
132
133
  yes: false,
133
134
  };
134
135
  const seen = new Set();
136
+ let verbose = false;
135
137
  for (let index = 0; index < argv.length; index += 1) {
136
138
  const argument = argv[index];
139
+ if (argument === "--verbose" && !verbose) {
140
+ verbose = true;
141
+ continue;
142
+ }
137
143
  const equals = argument.indexOf("=");
138
144
  const flag = equals === -1 ? argument : argument.slice(0, equals);
139
145
  const inline = equals === -1 ? undefined : argument.slice(equals + 1);
140
146
  const value = INIT_VALUES[flag];
141
147
  const toggle = INIT_SWITCHES[flag];
142
148
  if (value === undefined && toggle === undefined) {
143
- throw new CliCommandError(`unexpected argument ${JSON.stringify(argument)} for init; ${help}`);
149
+ throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)} for init; ${help}`);
144
150
  }
145
151
  const canonical = toggle === "yes" ? "--yes" : flag;
146
152
  if (seen.has(canonical)) {
147
- throw new CliCommandError(`${canonical} was given twice; ${help}`);
153
+ throw new AvCliCommandError(`${canonical} was given twice; ${help}`);
148
154
  }
149
155
  seen.add(canonical);
150
156
  if (toggle !== undefined) {
151
157
  if (inline !== undefined) {
152
- throw new CliCommandError(`${canonical} takes no value; ${help}`);
158
+ throw new AvCliCommandError(`${canonical} takes no value; ${help}`);
153
159
  }
154
160
  switches[toggle] = true;
155
161
  continue;
@@ -157,7 +163,7 @@ function parseInit(argv, help) {
157
163
  const taken = inline ?? argv[index + 1];
158
164
  if (taken === undefined ||
159
165
  (inline === undefined && taken.startsWith("-"))) {
160
- throw new CliCommandError(`${canonical} takes a value; ${help}`);
166
+ throw new AvCliCommandError(`${canonical} takes a value; ${help}`);
161
167
  }
162
168
  if (inline === undefined) {
163
169
  index += 1;
@@ -165,7 +171,12 @@ function parseInit(argv, help) {
165
171
  given[value] = taken;
166
172
  }
167
173
  if (switches.noEnvVar && given.envVar !== undefined) {
168
- throw new CliCommandError(`--env-var and --no-env-var contradict each other; ${help}`);
174
+ throw new AvCliCommandError(`--env-var and --no-env-var contradict each other; ${help}`);
169
175
  }
170
- return { command: "init", given, ...switches };
176
+ return {
177
+ command: "init",
178
+ given,
179
+ ...switches,
180
+ ...(verbose ? { verbose: true } : {}),
181
+ };
171
182
  }
@@ -1,5 +1,6 @@
1
1
  import type { ProcessEnvInput } from "../config/env.cascade.js";
2
2
  import type { CommandRunner } from "../init/command.runner.js";
3
+ import { type ProgressMode, type StepReporter } from "./step.reporter.js";
3
4
  /** What `runCli` reads and writes instead of the process globals. */
4
5
  export interface CliIo {
5
6
  readonly cwd: string;
@@ -14,11 +15,15 @@ export interface CliIo {
14
15
  readonly ask?: (prompt: string) => Promise<string>;
15
16
  /** Runs a command with stdout piped (`init`'s install); a child process by default. */
16
17
  readonly run?: CommandRunner;
18
+ /** How the steps are shown on stderr; none when omitted. */
19
+ readonly progress?: ProgressMode;
17
20
  }
21
+ /** How many steps a generation shows: the config, the contract, the emission, the write. */
22
+ export declare const GENERATION_STEPS = 4;
18
23
  /**
19
24
  * `avclient generate` — the generation, the generateAt rule's question, and the
20
25
  * report. Shared by `init`, which generates once the project is set up.
21
26
  *
22
27
  * @throws whatever the generation raises; the caller renders it.
23
28
  */
24
- export declare function runGenerate(io: CliIo, yes: boolean): Promise<number>;
29
+ export declare function runGenerate(io: CliIo, yes: boolean, reporter?: StepReporter): Promise<number>;