@aventara/client 0.1.0-pilot.3 → 1.0.0-rc.0

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 (60) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/README.md +29 -14
  3. package/dist/cli/command.parser.d.ts +4 -14
  4. package/dist/cli/command.parser.js +12 -1
  5. package/dist/cli/generate.command.d.ts +6 -1
  6. package/dist/cli/generate.command.js +12 -1
  7. package/dist/cli/generation-success.renderer.d.ts +0 -4
  8. package/dist/cli/step.reporter.d.ts +31 -0
  9. package/dist/cli/step.reporter.js +87 -0
  10. package/dist/cli/terminal.prompter.d.ts +0 -6
  11. package/dist/cli/warning.renderer.d.ts +0 -6
  12. package/dist/cli.d.ts +0 -16
  13. package/dist/cli.js +2 -0
  14. package/dist/config/client-config.interface.d.ts +10 -20
  15. package/dist/config/config.loader.d.ts +0 -21
  16. package/dist/config/config.resolver.d.ts +2 -17
  17. package/dist/config/env.cascade.d.ts +0 -30
  18. package/dist/config/module-style.resolver.d.ts +0 -25
  19. package/dist/config/tsconfig.locator.d.ts +0 -24
  20. package/dist/contract/contract.acceptance.d.ts +1 -40
  21. package/dist/contract/contract.acceptance.js +1 -1
  22. package/dist/contract/contract.fetcher.d.ts +0 -29
  23. package/dist/contract/contract.loader.d.ts +0 -7
  24. package/dist/emit/banner.emitter.d.ts +0 -18
  25. package/dist/emit/banner.emitter.js +1 -1
  26. package/dist/emit/client-surface.emitter.d.ts +0 -23
  27. package/dist/emit/client-surface.emitter.js +17 -6
  28. package/dist/emit/client-tree.emitter.d.ts +0 -20
  29. package/dist/emit/contract-carrier.emitter.d.ts +1 -8
  30. package/dist/emit/contract-carrier.emitter.js +1 -1
  31. package/dist/emit/derivation.emitter.d.ts +0 -28
  32. package/dist/emit/descriptor.emitter.d.ts +1 -1
  33. package/dist/emit/descriptor.emitter.js +1 -1
  34. package/dist/emit/emitted-tree.interface.d.ts +0 -53
  35. package/dist/emit/enum.emitter.d.ts +0 -20
  36. package/dist/emit/json-shape.emitter.d.ts +16 -0
  37. package/dist/emit/json-shape.emitter.js +88 -0
  38. package/dist/emit/module-specifier.scanner.d.ts +0 -15
  39. package/dist/emit/module-style.interface.d.ts +0 -15
  40. package/dist/emit/name.deriver.d.ts +1 -72
  41. package/dist/emit/name.deriver.js +1 -1
  42. package/dist/emit/named-type.emitter.d.ts +0 -20
  43. package/dist/emit/runtime.emitter.d.ts +1 -51
  44. package/dist/emit/runtime.emitter.js +15 -22
  45. package/dist/emit/scalar.codec.d.ts +0 -39
  46. package/dist/emit/scalar.codec.js +56 -13
  47. package/dist/emit/transaction.emitter.d.ts +0 -6
  48. package/dist/emit/transaction.emitter.js +112 -78
  49. package/dist/generate.d.ts +8 -36
  50. package/dist/generate.js +38 -24
  51. package/dist/index.d.ts +1 -5
  52. package/dist/init/client-config.template.d.ts +0 -8
  53. package/dist/init/client-init.orchestrator.js +31 -16
  54. package/dist/init/client-init.planner.d.ts +0 -1
  55. package/dist/init/client-init.questions.d.ts +0 -8
  56. package/dist/init/command.runner.d.ts +2 -0
  57. package/dist/init/command.runner.js +4 -1
  58. package/dist/output/output.validator.d.ts +0 -39
  59. package/dist/output/output.writer.d.ts +1 -110
  60. package/package.json +6 -5
@@ -1,4 +1,4 @@
1
- import { BUILT_IN_SCALARS } from "@aventara/core";
1
+ import { BUILT_IN_SCALARS } from "@aventara/core/internal";
2
2
  import { AvProtocol } from "@aventara/core/protocol";
3
3
  import { moduleSpecifierWriter, } from "./module-style.interface.js";
4
4
  export function wireGrammarLiteral(scalar) {
@@ -331,31 +331,74 @@ export function encodeArguments(value: unknown): unknown {
331
331
  return encodeValue(value, [], new Set());
332
332
  }
333
333
 
334
- function encodeValue(value: unknown, path: Path, ancestors: Set<object>): unknown {
335
- if (typeof value === "bigint") {
336
- return encodeScalar("bigint", value);
334
+ /**
335
+ * One read of a caller's arguments. Every native read on them happens through it,
336
+ * here and in the transaction builder, so a value that cannot be read — a revoked
337
+ * Proxy (an Immer draft after produce), a throwing getter or trap — is refused at its
338
+ * path with a TypeError before anything is sent, never with the engine's own error.
339
+ */
340
+ export function readArgument<T>(path: Path, read: () => T): T {
341
+ try {
342
+ return read();
343
+ } catch {
344
+ throw new TypeError(
345
+ "An operation's arguments must be readable plain data, and the value at " + pointerOf(path) + " could not be read.",
346
+ );
337
347
  }
348
+ }
349
+
350
+ /** How encodeValue treats an object: a scalar's runtime class, a container, or neither. */
351
+ function objectKind(value: object): "datetime" | "decimal" | "bytes" | "container" | "other" {
338
352
  if (value instanceof Date) {
339
- return encodeScalar("datetime", value);
353
+ return "datetime";
340
354
  }
341
355
  if (value instanceof Decimal) {
342
- return encodeScalar("decimal", value);
356
+ return "decimal";
343
357
  }
344
358
  if (value instanceof Uint8Array) {
345
- return encodeScalar("bytes", value);
359
+ return "bytes";
346
360
  }
347
- if (typeof value !== "object" || value === null || !isJsonContainer(value)) {
361
+ return isJsonContainer(value) ? "container" : "other";
362
+ }
363
+
364
+ function encodeValue(value: unknown, path: Path, ancestors: Set<object>): unknown {
365
+ if (typeof value === "bigint") {
366
+ return encodeScalar("bigint", value);
367
+ }
368
+ if (typeof value !== "object" || value === null) {
369
+ return value;
370
+ }
371
+ const kind = readArgument(path, () => objectKind(value));
372
+ if (kind === "other") {
348
373
  return value;
349
374
  }
375
+ if (kind !== "container") {
376
+ return encodeScalar(kind, value);
377
+ }
350
378
  if (ancestors.has(value)) {
351
379
  return refuseBody(path, "a cycle");
352
380
  }
353
381
  ancestors.add(value);
354
- const copy = Array.isArray(value)
355
- ? value.map((item, index) => encodeValue(item, [...path, index], ancestors))
356
- : Object.fromEntries(
357
- Object.entries(value).map(([key, member]) => [key, encodeValue(member, [...path, key], ancestors)]),
358
- );
382
+ let copy: unknown;
383
+ if (readArgument(path, () => Array.isArray(value))) {
384
+ const items = value as readonly unknown[];
385
+ const length = readArgument(path, () => items.length);
386
+ const list: unknown[] = [];
387
+ for (let index = 0; index < length; index += 1) {
388
+ const itemPath = [...path, index];
389
+ list.push(encodeValue(readArgument(itemPath, () => items[index]), itemPath, ancestors));
390
+ }
391
+ copy = list;
392
+ } else {
393
+ const record = value as Readonly<Record<string, unknown>>;
394
+ const members: [string, unknown][] = [];
395
+ for (const key of readArgument(path, () => Object.keys(record))) {
396
+ const memberPath = [...path, key];
397
+ members.push([key, encodeValue(readArgument(memberPath, () => record[key]), memberPath, ancestors)]);
398
+ }
399
+ // fromEntries defines each key, so "__proto__" stays an own key.
400
+ copy = Object.fromEntries(members);
401
+ }
359
402
  ancestors.delete(value);
360
403
  return copy;
361
404
  }
@@ -1,9 +1,3 @@
1
1
  import type { EmittedModule } from "./emitted-tree.interface.js";
2
2
  import { type ClientModuleStyle } from "./module-style.interface.js";
3
- /**
4
- * The transaction builder and runner: `runtime/fingerprint.ts` and
5
- * `runtime/transaction.ts`, emitted iff the ClientContract advertises
6
- * `interactive` transactions — `client.ts` wires them to `avClient.tx` and
7
- * `avClient.transaction`.
8
- */
9
3
  export declare function emitTransactionModules(style: ClientModuleStyle): readonly EmittedModule[];
@@ -12,8 +12,7 @@ const FINGERPRINT_MODULE = `/**
12
12
  * The v1 operation fingerprint: RFC 8785 canonical JSON of
13
13
  * { resource, family, variant, args } over the WIRE-form arguments, its UTF-8
14
14
  * bytes hashed with SHA-256, the first 16 bytes in base64url, prefixed "fp1:".
15
- * Dependency-free, and pinned byte for byte to core's
16
- * computeOperationFingerprint.
15
+ * Dependency-free; the same bytes the server computes.
17
16
  */
18
17
 
19
18
  /** One plan node, its arguments already in wire form. */
@@ -210,26 +209,29 @@ function base64Url(bytes: Uint8Array): string {
210
209
  }
211
210
  `;
212
211
  function transactionModuleSource(from) {
213
- return `import { encodeArguments, serializeWireBody } from ${from("./codec")};
212
+ return `import { encodeArguments, readArgument, serializeWireBody } from ${from("./codec")};
214
213
  import { type Cause, type FrameworkError, frameworkErrorOf } from ${from("./errors")};
215
214
  import { fingerprintOf } from ${from("./fingerprint")};
216
215
  import { type CallOptions, executePlan, type TransportConnection } from ${from("./transport")};
217
216
 
218
217
  /**
219
- * The deferred transaction builder and runner, as core assembles a plan
220
- * in process (transaction-plan-assembler.ts): \`avClient.tx\` defers an operation
218
+ * The deferred transaction builder and runner: \`avClient.tx\` defers an operation
221
219
  * into a handle — pure data, no request — and \`avClient.transaction\` binds a
222
220
  * list of handles into one transaction plan, sends it once, and resolves one
223
221
  * result per handle in list order. A handle has no connection, so any client of
224
222
  * this generated tree may run it.
225
223
  */
226
224
 
227
- /** What a handle defers. */
225
+ /**
226
+ * What a handle defers — or why its arguments could not be read: the builder
227
+ * never throws into app code, and running the handle refuses it instead.
228
+ */
228
229
  interface DeferredRecord {
229
230
  readonly resource: string;
230
231
  readonly family: string;
231
232
  readonly variant: string;
232
233
  readonly args: unknown;
234
+ readonly unreadable?: TypeError;
233
235
  }
234
236
 
235
237
  /** What a \`$ref\` placeholder stands for until the plan is assembled. */
@@ -238,8 +240,6 @@ interface Placeholder {
238
240
  readonly field: string;
239
241
  }
240
242
 
241
- type MutableRecord = Record<string, unknown>;
242
-
243
243
  /** One wire node of a transaction plan, its arguments in wire form. */
244
244
  interface PlanNode {
245
245
  readonly resource: string;
@@ -249,10 +249,11 @@ interface PlanNode {
249
249
  readonly fingerprint: string | null;
250
250
  }
251
251
 
252
- /** One reference container: a mutation-data record and its path inside the arguments. */
253
- interface Container {
252
+ /** One placeholder an argument tree holds: where, the object itself, and its meaning. */
253
+ interface PlaceholderSite {
254
254
  readonly path: readonly (string | number)[];
255
- readonly record: Readonly<Record<string, unknown>>;
255
+ readonly object: object;
256
+ readonly placeholder: Placeholder;
256
257
  }
257
258
 
258
259
  const HANDLES = new WeakMap<object, DeferredRecord>();
@@ -266,45 +267,73 @@ function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown
266
267
  return prototype === Object.prototype || prototype === null;
267
268
  }
268
269
 
269
- /** The argument keys holding reference positions: the top-level mutation data. */
270
- function referenceKeys(family: string): readonly string[] {
271
- return family === "create" || family === "update" ? ["data"] : family === "upsert" ? ["create", "update"] : [];
272
- }
270
+ const KEEP: unique symbol = Symbol("keep");
273
271
 
274
- function containers(family: string, args: unknown): readonly Container[] {
275
- if (!isPlainRecord(args)) {
276
- return [];
272
+ /**
273
+ * Rebuilds an argument tree: every plain record and array is copied, every other
274
+ * value kept, and \`leaf\` may replace any value first (a placeholder, by identity).
275
+ * A placeholder is bound wherever the arguments hold one; the server answers a
276
+ * position that takes no reference. A container met again on its own ancestry is
277
+ * kept as it is; the codec refuses it.
278
+ */
279
+ function rebuild(
280
+ value: unknown,
281
+ path: readonly (string | number)[],
282
+ leaf: (value: unknown, path: readonly (string | number)[]) => unknown,
283
+ ancestors: Set<object> = new Set(),
284
+ ): unknown {
285
+ const replaced = leaf(value, path);
286
+ if (replaced !== KEEP || typeof value !== "object" || value === null) {
287
+ return replaced === KEEP ? value : replaced;
277
288
  }
278
- return referenceKeys(family).flatMap((key): Container[] => {
279
- const value = args[key];
280
- if (Array.isArray(value)) {
281
- return value.flatMap((entry: unknown, index) => (isPlainRecord(entry) ? [{ path: [key, index], record: entry }] : []));
289
+ const array = readArgument(path, () => Array.isArray(value));
290
+ if ((!array && !readArgument(path, () => isPlainRecord(value))) || ancestors.has(value)) {
291
+ return value;
292
+ }
293
+ ancestors.add(value);
294
+ let copy: unknown;
295
+ if (array) {
296
+ const items = value as readonly unknown[];
297
+ const length = readArgument(path, () => items.length);
298
+ const list: unknown[] = [];
299
+ for (let index = 0; index < length; index += 1) {
300
+ const itemPath = [...path, index];
301
+ list.push(rebuild(readArgument(itemPath, () => items[index]), itemPath, leaf, ancestors));
282
302
  }
283
- return isPlainRecord(value) ? [{ path: [key], record: value }] : [];
284
- });
303
+ copy = list;
304
+ } else {
305
+ const record = value as Readonly<Record<string, unknown>>;
306
+ const members: [string, unknown][] = [];
307
+ for (const key of readArgument(path, () => Object.keys(record))) {
308
+ const memberPath = [...path, key];
309
+ members.push([key, rebuild(readArgument(memberPath, () => record[key]), memberPath, leaf, ancestors)]);
310
+ }
311
+ copy = Object.fromEntries(members);
312
+ }
313
+ ancestors.delete(value);
314
+ return copy;
285
315
  }
286
316
 
287
- /** \`args\` with each reference container replaced by \`rebuild\`, cloning only the containing path. */
288
- function replaceContainers(family: string, args: unknown, rebuild: (container: Container) => MutableRecord): unknown {
289
- if (!isPlainRecord(args)) {
290
- return args;
291
- }
292
- const result: MutableRecord = { ...args };
293
- for (const key of referenceKeys(family)) {
294
- const value = args[key];
295
- if (Array.isArray(value)) {
296
- result[key] = value.map((entry: unknown, index) => (isPlainRecord(entry) ? rebuild({ path: [key, index], record: entry }) : entry));
297
- } else if (isPlainRecord(value)) {
298
- result[key] = rebuild({ path: [key], record: value });
317
+ /** Every placeholder \`args\` holds, with its path inside the arguments. */
318
+ function placeholderSites(args: unknown): readonly PlaceholderSite[] {
319
+ const sites: PlaceholderSite[] = [];
320
+ rebuild(args, [], (value, path) => {
321
+ const placeholder = typeof value === "object" && value !== null ? PLACEHOLDERS.get(value) : undefined;
322
+ if (placeholder === undefined) {
323
+ return KEEP;
299
324
  }
300
- }
301
- return result;
325
+ sites.push({ path, object: value as object, placeholder });
326
+ return value;
327
+ });
328
+ return sites;
302
329
  }
303
330
 
304
331
  /**
305
- * Defers one operation into a handle: nothing is sent. Its reference
306
- * containers are copied now, so a later change to the caller's data cannot move
307
- * a reference — and a handle can reference only handles built before it.
332
+ * Defers one operation into a handle: nothing is sent, and nothing is thrown.
333
+ * Its arguments are copied now, so a later change to the caller's data cannot
334
+ * move a reference — and a handle can reference only handles built before it.
335
+ * Arguments that cannot be read are kept as their refusal, which running the
336
+ * handle rejects with.
308
337
  */
309
338
  export function deferOperation(resource: string, family: string, variant: string, args: unknown): object {
310
339
  const handle: object = Object.freeze({
@@ -314,12 +343,19 @@ export function deferOperation(resource: string, family: string, variant: string
314
343
  return placeholder;
315
344
  },
316
345
  });
317
- HANDLES.set(handle, {
318
- resource,
319
- family,
320
- variant,
321
- args: replaceContainers(family, args, ({ record }) => ({ ...record })),
322
- });
346
+ let record: DeferredRecord;
347
+ try {
348
+ record = {
349
+ resource,
350
+ family,
351
+ variant,
352
+ args: rebuild(args, [], (value) => (typeof value === "object" && value !== null && PLACEHOLDERS.has(value) ? value : KEEP)),
353
+ };
354
+ } catch (error) {
355
+ // Only readArgument throws here: an argument that could not be read.
356
+ record = { resource, family, variant, args: {}, unreadable: error as TypeError };
357
+ }
358
+ HANDLES.set(handle, record);
323
359
  return handle;
324
360
  }
325
361
 
@@ -327,7 +363,7 @@ export function deferOperation(resource: string, family: string, variant: string
327
363
  function entryRefusal(operation: number, message: string): FrameworkError {
328
364
  const cause: Cause = {
329
365
  message: "Transaction plan failed framework validation.",
330
- issues: [{ path: ["operation"], code: "V1001", message }],
366
+ issues: [{ path: ["operations", operation], code: "V1001", message }],
331
367
  operation,
332
368
  };
333
369
  return frameworkErrorOf("A2004", cause);
@@ -338,8 +374,9 @@ function entryRefusal(operation: number, message: string): FrameworkError {
338
374
  * step. Refused before anything is sent, with what core answers in
339
375
  * process: a list that is not one, an entry that is not a handle of this
340
376
  * generated client, one handle twice (ValidationError A2004 / V1001 at
341
- * ["operation"]); a reference to a handle not in the list (ValidationError A2007 /
342
- * V1010 at its \`$ref\`).
377
+ * ["operations", i]: the entry itself); a reference to a handle not in the list (ValidationError A2007 /
378
+ * V1010 at its \`$ref\`); a handle whose arguments could not be read when it was
379
+ * built (the TypeError naming where).
343
380
  */
344
381
  export async function runTransaction(
345
382
  connection: TransportConnection,
@@ -366,27 +403,27 @@ export async function runTransaction(
366
403
  records.push(record);
367
404
  }
368
405
  for (const [index, record] of records.entries()) {
369
- for (const { path, record: container } of containers(record.family, record.args)) {
370
- for (const [field, value] of Object.entries(container)) {
371
- const placeholder = typeof value === "object" && value !== null ? PLACEHOLDERS.get(value) : undefined;
372
- if (placeholder !== undefined && !positions.has(placeholder.source)) {
373
- throw frameworkErrorOf("A2007", {
374
- message: "Operation arguments failed framework validation.",
375
- issues: [
376
- {
377
- path: ["operations", index, "args", ...path, field, "$ref"],
378
- code: "V1010",
379
- message: "Transaction reference names an operation that is not in this transaction.",
380
- },
381
- ],
382
- operation: index,
383
- });
384
- }
406
+ if (record.unreadable !== undefined) {
407
+ throw record.unreadable;
408
+ }
409
+ for (const { path, placeholder } of placeholderSites(record.args)) {
410
+ if (!positions.has(placeholder.source)) {
411
+ throw frameworkErrorOf("A2007", {
412
+ message: "Operation arguments failed framework validation.",
413
+ issues: [
414
+ {
415
+ path: ["operations", index, "args", ...path, "$ref"],
416
+ code: "V1010",
417
+ message: "Transaction reference names an operation that is not in this transaction.",
418
+ },
419
+ ],
420
+ operation: index,
421
+ });
385
422
  }
386
423
  }
387
424
  }
388
425
 
389
- // Bound lazily, as core binds them: a node's references need their source's
426
+ // Bound lazily: a node's references need their source's
390
427
  // fingerprint, so each node is built once, on first need, wherever its source
391
428
  // stands in the list — the server, not this client, refuses a later one.
392
429
  const nodes = new Map<number, PlanNode>();
@@ -396,17 +433,14 @@ export async function runTransaction(
396
433
  return built;
397
434
  }
398
435
  const record = records[index] as DeferredRecord;
399
- const bound = replaceContainers(record.family, record.args, ({ record: container }) => {
400
- const copy: MutableRecord = { ...container };
401
- for (const [field, value] of Object.entries(container)) {
402
- const placeholder = typeof value === "object" && value !== null ? PLACEHOLDERS.get(value) : undefined;
403
- if (placeholder !== undefined) {
404
- const operation = positions.get(placeholder.source) as number;
405
- copy[field] = { $ref: { operation, fingerprint: nodeAt(operation).fingerprint, path: [placeholder.field] } };
406
- }
407
- }
408
- return copy;
409
- });
436
+ const references = new Map<object, unknown>();
437
+ for (const { object, placeholder } of placeholderSites(record.args)) {
438
+ const operation = positions.get(placeholder.source) as number;
439
+ references.set(object, { $ref: { operation, fingerprint: nodeAt(operation).fingerprint, path: [placeholder.field] } });
440
+ }
441
+ const bound = rebuild(record.args, [], (value) =>
442
+ typeof value === "object" && value !== null && references.has(value) ? references.get(value) : KEEP,
443
+ );
410
444
  const args: unknown = JSON.parse(serializeWireBody(encodeArguments(bound)));
411
445
  const node: PlanNode = {
412
446
  resource: record.resource,
@@ -1,23 +1,8 @@
1
1
  import type { AbsolutePath } from "./config/client-config.interface.js";
2
2
  import { type ProcessEnvInput } from "./config/env.cascade.js";
3
- import type { ContractFetch } from "./contract/contract.fetcher.js";
3
+ import { type ContractFetch } from "./contract/contract.fetcher.js";
4
4
  import { type EmittedFilePath } from "./emit/emitted-tree.interface.js";
5
5
  import type { OutputCheck, TypeScriptResolver } from "./output/output.validator.js";
6
- /**
7
- * The cascade is a returned record, never written into `process.env`: the config
8
- * reaches it through `env("NAME")`.
9
- *
10
- * Output goes to `generateAt`: `AvClient.ts` and `generated/`, nothing else there
11
- * touched. Content in those two the generator did not produce is never overwritten
12
- * unasked: the run returns `ForeignOutputContent` instead, and the caller decides.
13
- *
14
- * Every failure is thrown, every diagnostic that is not a failure is returned:
15
- * this function prints nothing and asks nothing. `cli.ts` owns the terminal. A
16
- * refusal raised after a warning (`OutputWriteError`) carries the warnings raised
17
- * before it, and so does `ForeignOutputContent`; a defect is rethrown as itself,
18
- * its warnings kept beside it (`warningsRaisedBeforeDefect`). No run's warnings
19
- * are lost because it stopped.
20
- */
21
6
  export interface ClientGenerationInput {
22
7
  /** The project directory: where `framework.client.ts` and the `.env` files are. */
23
8
  readonly directory: string;
@@ -30,14 +15,17 @@ export interface ClientGenerationInput {
30
15
  * written.
31
16
  */
32
17
  readonly overrideForeign?: boolean;
33
- /** Injected for tests; the platform `fetch` otherwise. */
34
18
  readonly fetch?: ContractFetch;
19
+ readonly resolveTypeScript?: TypeScriptResolver;
35
20
  /**
36
- * The `typescript` optional peer; by default the project's own, resolved from
37
- * `generateAt`.
21
+ * Runs each of the run's four phases — the config, the contract, the
22
+ * emission, the write — under a title, so whoever owns the terminal can show
23
+ * them (`cli/step.reporter.ts`); each phase simply runs when omitted.
38
24
  */
39
- readonly resolveTypeScript?: TypeScriptResolver;
25
+ readonly step?: GenerationStep;
40
26
  }
27
+ /** Runs one phase of a generation under its title, resolving to what it returns. */
28
+ export type GenerationStep = <T>(title: string, body: () => Promise<T> | T) => Promise<T>;
41
29
  export interface ClientGenerated {
42
30
  readonly kind: "generated";
43
31
  /** The shared directory the client was generated into. */
@@ -46,11 +34,6 @@ export interface ClientGenerated {
46
34
  readonly files: readonly EmittedFilePath[];
47
35
  /** How deeply the output was validated before it replaced the previous one. */
48
36
  readonly checked: OutputCheck;
49
- /**
50
- * The deployment answered `304` — the ClientContract is the one the previous
51
- * output was generated against — and the output was still replaced, because this
52
- * generator or this entrypoint emits other bytes.
53
- */
54
37
  readonly contractUnchanged: boolean;
55
38
  /**
56
39
  * Everything the run has to say without failing, in a fixed order: the
@@ -59,12 +42,6 @@ export interface ClientGenerated {
59
42
  */
60
43
  readonly warnings: readonly string[];
61
44
  }
62
- /**
63
- * The run found content in `AvClient.ts` or `generated/` the generator did not
64
- * produce, and stopped before writing anything. Whoever owns the terminal asks;
65
- * `proceed` writes the emission already made — no second fetch — overwriting or
66
- * removing exactly `foreign`, through the same temp → validate → replace path.
67
- */
68
45
  export interface ForeignOutputContent {
69
46
  readonly kind: "foreign-content";
70
47
  readonly generateAt: AbsolutePath;
@@ -82,11 +59,6 @@ export interface ForeignOutputContent {
82
59
  readonly warnings: readonly string[];
83
60
  proceed(): Promise<ClientGenerated>;
84
61
  }
85
- /**
86
- * The deployment serves the ClientContract the output was generated against, and
87
- * this generator, with this entrypoint, emits exactly the bytes already there:
88
- * nothing was written.
89
- */
90
62
  export interface ClientUpToDate {
91
63
  readonly kind: "up-to-date";
92
64
  readonly generateAt: AbsolutePath;
package/dist/generate.js CHANGED
@@ -4,11 +4,13 @@ import { loadClientConfigFile } from "./config/config.loader.js";
4
4
  import { resolveClientConfig } from "./config/config.resolver.js";
5
5
  import { resolveEnvCascade, } from "./config/env.cascade.js";
6
6
  import { resolveModuleStyle } from "./config/module-style.resolver.js";
7
+ import { contractUrlOf, displayUrl, } from "./contract/contract.fetcher.js";
7
8
  import { loadClientContractSince } from "./contract/contract.loader.js";
8
9
  import { emitClientTree } from "./emit/client-tree.emitter.js";
9
10
  import { parseContractCarrier } from "./emit/contract-carrier.emitter.js";
10
11
  import { CLIENT_ENTRY_FILE, CONTRACT_CARRIER_MODULE, GENERATED_DIRECTORY, } from "./emit/emitted-tree.interface.js";
11
12
  import { findForeignOutputContent, OutputWriteError, ownedOutputMatches, precedeDefect, writeClientOutput, } from "./output/output.writer.js";
13
+ const runPhase = async (_title, body) => await body();
12
14
  async function storedContract(generateAt) {
13
15
  try {
14
16
  if ((await findForeignOutputContent(generateAt)).length > 0) {
@@ -21,32 +23,42 @@ async function storedContract(generateAt) {
21
23
  }
22
24
  }
23
25
  export async function generateClient(input) {
24
- const cascade = resolveEnvCascade({
25
- directory: input.directory,
26
- processEnv: input.processEnv,
27
- });
28
- const loaded = await loadClientConfigFile(input.directory);
29
- const config = resolveClientConfig({
30
- config: loaded.config,
31
- cascade,
32
- configDirectory: path.dirname(loaded.file),
33
- });
34
- const style = resolveModuleStyle({
35
- generateAt: config.generateAt,
36
- entryFile: path.join(config.generateAt, CLIENT_ENTRY_FILE),
37
- ...(config.tsconfigFile === undefined
38
- ? {}
39
- : { tsconfigFile: config.tsconfigFile }),
26
+ const step = input.step ?? runPhase;
27
+ const { config, style } = await step("Loading the client config", async () => {
28
+ const cascade = resolveEnvCascade({
29
+ directory: input.directory,
30
+ processEnv: input.processEnv,
31
+ });
32
+ const loaded = await loadClientConfigFile(input.directory);
33
+ const config = resolveClientConfig({
34
+ config: loaded.config,
35
+ cascade,
36
+ configDirectory: path.dirname(loaded.file),
37
+ });
38
+ const style = resolveModuleStyle({
39
+ generateAt: config.generateAt,
40
+ entryFile: path.join(config.generateAt, CLIENT_ENTRY_FILE),
41
+ ...(config.tsconfigFile === undefined
42
+ ? {}
43
+ : { tsconfigFile: config.tsconfigFile }),
44
+ });
45
+ return { config, style };
40
46
  });
41
- const { contract, notModified } = await loadClientContractSince(input.fetch === undefined
47
+ const { contract, notModified } = await step(`Fetching the ClientContract (${displayUrl(contractUrlOf(config.entrypoint))})`, async () => loadClientContractSince(input.fetch === undefined
42
48
  ? { entrypoint: config.entrypoint }
43
- : { entrypoint: config.entrypoint, fetch: input.fetch }, await storedContract(config.generateAt));
44
- const emission = emitClientTree(contract, config.entrypoint, {
45
- importFileExtension: style.importFileExtension,
46
- moduleFormat: style.moduleFormat,
49
+ : { entrypoint: config.entrypoint, fetch: input.fetch }, await storedContract(config.generateAt)));
50
+ const { emission, upToDate } = await step("Emitting the client", async () => {
51
+ const emission = emitClientTree(contract, config.entrypoint, {
52
+ importFileExtension: style.importFileExtension,
53
+ moduleFormat: style.moduleFormat,
54
+ });
55
+ return {
56
+ emission,
57
+ upToDate: notModified &&
58
+ (await ownedOutputMatches(config.generateAt, emission.tree)),
59
+ };
47
60
  });
48
- if (notModified &&
49
- (await ownedOutputMatches(config.generateAt, emission.tree))) {
61
+ if (upToDate) {
50
62
  return {
51
63
  kind: "up-to-date",
52
64
  generateAt: config.generateAt,
@@ -54,7 +66,7 @@ export async function generateClient(input) {
54
66
  warnings: emission.warnings,
55
67
  };
56
68
  }
57
- const write = async (overrideForeign) => {
69
+ const writeInto = async (overrideForeign) => {
58
70
  const written = await writeClientOutput(input.resolveTypeScript === undefined
59
71
  ? { emission, generateAt: config.generateAt, overrideForeign }
60
72
  : {
@@ -72,6 +84,8 @@ export async function generateClient(input) {
72
84
  warnings: written.warnings,
73
85
  };
74
86
  };
87
+ const into = path.relative(input.directory, config.generateAt) || ".";
88
+ const write = (overrideForeign) => step(`Checking and writing the client into ${into}`, () => writeInto(overrideForeign));
75
89
  const foreign = await findForeignOutputContent(config.generateAt).catch((error) => {
76
90
  throw error instanceof OutputWriteError
77
91
  ? error.precededBy(emission.warnings)
package/dist/index.d.ts CHANGED
@@ -1,8 +1,4 @@
1
- /**
2
- * This package's version, read from its own manifest — the `package.json` every
3
- * tarball carries beside `dist/` — so a release's `changeset version` is the one
4
- * statement of it.
5
- */
1
+ /** The version of `@aventara/client`. */
6
2
  export declare const AVENTARA_CLIENT_GENERATOR_VERSION: string;
7
3
  export type { ClientConfigInput, ConfigValue, EnvReference, } from "./config/client-config.interface.js";
8
4
  export { defineClientConfig, env } from "./config/config.resolver.js";
@@ -1,11 +1,3 @@
1
- /**
2
- * `file` names it in its own comment line: the `framework.client.ts` it writes, or
3
- * the project's own config of another extension, which init reuses rather than add
4
- * a second (two configs make `avclient generate` refuse). A `.cjs` one is written
5
- * in CommonJS — `require` and `module.exports` — the syntax its extension
6
- * promises; every other one in ES module syntax, which the loader reads in any of
7
- * them.
8
- */
9
1
  export declare function clientConfigSource(input: {
10
2
  readonly entrypoint: string;
11
3
  readonly envVar: string | undefined;