@aventara/client 1.0.0-rc.0 → 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.
- package/CHANGELOG.md +457 -0
- package/README.md +159 -141
- package/dist/cli/command.parser.d.ts +3 -3
- package/dist/cli/command.parser.js +10 -10
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/config/client-config.interface.d.ts +7 -7
- package/dist/config/config.loader.d.ts +4 -4
- package/dist/config/config.loader.js +7 -7
- package/dist/config/config.resolver.d.ts +7 -7
- package/dist/config/config.resolver.js +12 -12
- package/dist/config/env.cascade.d.ts +2 -2
- package/dist/config/env.cascade.js +4 -4
- package/dist/config/module-style.resolver.d.ts +1 -1
- package/dist/config/module-style.resolver.js +2 -2
- package/dist/config/tsconfig.locator.d.ts +1 -1
- package/dist/config/tsconfig.locator.js +2 -2
- package/dist/contract/contract.acceptance.d.ts +4 -4
- package/dist/contract/contract.acceptance.js +4 -4
- package/dist/contract/contract.fetcher.d.ts +3 -3
- package/dist/contract/contract.fetcher.js +6 -6
- package/dist/contract/contract.loader.d.ts +6 -6
- package/dist/contract/contract.loader.js +2 -2
- package/dist/emit/client-surface.emitter.d.ts +2 -2
- package/dist/emit/client-surface.emitter.js +63 -40
- package/dist/emit/client-tree.emitter.d.ts +3 -3
- package/dist/emit/client-tree.emitter.js +8 -5
- package/dist/emit/contract-carrier.emitter.d.ts +3 -3
- package/dist/emit/derivation.emitter.js +1 -0
- package/dist/emit/descriptor.emitter.d.ts +2 -2
- package/dist/emit/descriptor.emitter.js +1 -1
- package/dist/emit/emitted-tree.interface.d.ts +1 -1
- package/dist/emit/enum.emitter.d.ts +2 -2
- package/dist/emit/field-check.emitter.d.ts +21 -0
- package/dist/emit/field-check.emitter.js +603 -0
- package/dist/emit/json-shape.emitter.d.ts +2 -2
- package/dist/emit/name.deriver.d.ts +6 -6
- package/dist/emit/name.deriver.js +14 -14
- package/dist/emit/named-type.emitter.d.ts +2 -2
- package/dist/emit/runtime.emitter.d.ts +2 -2
- package/dist/emit/runtime.emitter.js +115 -110
- package/dist/emit/scalar.codec.d.ts +1 -1
- package/dist/emit/scalar.codec.js +24 -24
- package/dist/emit/transaction.emitter.js +26 -14
- package/dist/generate.d.ts +2 -2
- package/dist/generate.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/init/client-init.errors.d.ts +4 -4
- package/dist/init/client-init.errors.js +4 -4
- package/dist/init/client-init.orchestrator.js +7 -7
- package/dist/init/client-init.questions.d.ts +5 -5
- package/dist/init/client-init.questions.js +10 -10
- package/dist/init/client-project.inspector.d.ts +2 -2
- package/dist/init/client-project.inspector.js +3 -3
- package/dist/output/output.writer.d.ts +5 -5
- package/dist/output/output.writer.js +12 -12
- package/package.json +3 -3
|
@@ -29,7 +29,7 @@ const SCALAR_CODEC_SOURCES = {
|
|
|
29
29
|
: refuse("bigint", "wire", "a base-10 integer string")`,
|
|
30
30
|
},
|
|
31
31
|
decimal: {
|
|
32
|
-
encode: `(value) => (value instanceof
|
|
32
|
+
encode: `(value) => (value instanceof AvDecimal ? value.toString() : refuse("decimal", "runtime", "an AvDecimal"))`,
|
|
33
33
|
decode: "decodeDecimal",
|
|
34
34
|
},
|
|
35
35
|
datetime: {
|
|
@@ -48,13 +48,13 @@ const SCALAR_CODEC_SOURCES = {
|
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
function codecModuleHead(from) {
|
|
51
|
-
return `import {
|
|
51
|
+
return `import { AvDecimal } from ${from("./decimal")};
|
|
52
52
|
import type { FieldDecoding } from ${from("./descriptor")};
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* The scalar codecs of Aventara protocol version 1: one per built-in
|
|
56
56
|
* scalar, converting between the generated runtime value and the JSON wire value.
|
|
57
|
-
* A value is never wrapped per field
|
|
57
|
+
* A value is never wrapped per field: the Contract names the scalar, so the
|
|
58
58
|
* caller says which scalar a value is.
|
|
59
59
|
*
|
|
60
60
|
* null is explicit in both directions and passes through; whether a field admits
|
|
@@ -82,7 +82,7 @@ const DATETIME_WIRE = ${wireGrammarLiteral("datetime")};
|
|
|
82
82
|
const BASE64_WIRE = ${wireGrammarLiteral("bytes")};
|
|
83
83
|
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
84
84
|
|
|
85
|
-
function refuse(scalar:
|
|
85
|
+
function refuse(scalar: AvBuiltInScalar, side: Side, expected: string): never {
|
|
86
86
|
throw new TypeError('A "' + scalar + '" ' + side + " value must be " + expected + ".");
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -94,10 +94,10 @@ function isFiniteNumber(value: unknown): value is number {
|
|
|
94
94
|
return typeof value === "number" && Number.isFinite(value);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function decodeDecimal(wire: unknown):
|
|
97
|
+
function decodeDecimal(wire: unknown): AvDecimal {
|
|
98
98
|
if (typeof wire === "string") {
|
|
99
99
|
try {
|
|
100
|
-
return new
|
|
100
|
+
return new AvDecimal(wire);
|
|
101
101
|
} catch {
|
|
102
102
|
// Refused below, naming the scalar.
|
|
103
103
|
}
|
|
@@ -195,8 +195,8 @@ function kindOf(value: unknown): string {
|
|
|
195
195
|
if (value instanceof Uint8Array) {
|
|
196
196
|
return "a Uint8Array";
|
|
197
197
|
}
|
|
198
|
-
if (value instanceof
|
|
199
|
-
return "
|
|
198
|
+
if (value instanceof AvDecimal) {
|
|
199
|
+
return "an AvDecimal";
|
|
200
200
|
}
|
|
201
201
|
if (typeof value === "object" && value !== null && isPlainRecord(value)) {
|
|
202
202
|
return "a symbol-keyed property";
|
|
@@ -288,7 +288,7 @@ function writeWireJson(value: unknown, path: Path, ancestors: Set<object>): stri
|
|
|
288
288
|
return text;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
function codecOf(scalar:
|
|
291
|
+
function codecOf(scalar: AvBuiltInScalar): ScalarCodec {
|
|
292
292
|
if (!Object.hasOwn(SCALAR_CODECS, scalar)) {
|
|
293
293
|
throw new TypeError('"' + String(scalar) + '" is not a built-in scalar of this protocol version.');
|
|
294
294
|
}
|
|
@@ -296,21 +296,21 @@ function codecOf(scalar: BuiltInScalar): ScalarCodec {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/** The JSON wire value of a scalar's runtime value; null passes through. */
|
|
299
|
-
export function encodeScalar(scalar:
|
|
299
|
+
export function encodeScalar(scalar: AvBuiltInScalar, value: unknown): unknown {
|
|
300
300
|
const codec = codecOf(scalar);
|
|
301
301
|
return value === null ? null : codec.encode(value);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
/** The runtime value of a scalar's JSON wire value; null passes through. */
|
|
305
|
-
export function decodeScalar(scalar:
|
|
305
|
+
export function decodeScalar(scalar: AvBuiltInScalar, wire: unknown): unknown {
|
|
306
306
|
const codec = codecOf(scalar);
|
|
307
307
|
return wire === null ? null : codec.decode(wire);
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/**
|
|
311
311
|
* The JSON text of a request body whose scalars are already encoded. Every
|
|
312
|
-
* undefined property is stripped, at every depth
|
|
313
|
-
* omitted, never sent
|
|
312
|
+
* undefined property is stripped, at every depth (an optional property is
|
|
313
|
+
* omitted, never sent), and null is kept as the explicit value it is.
|
|
314
314
|
* Anything else that is not JSON-safe is refused rather than coerced: it means a
|
|
315
315
|
* value escaped its scalar codec.
|
|
316
316
|
*/
|
|
@@ -321,8 +321,8 @@ export function serializeWireBody(body: unknown): string {
|
|
|
321
321
|
/**
|
|
322
322
|
* An operation's arguments with every scalar in its wire form, read off each
|
|
323
323
|
* value's runtime class: a
|
|
324
|
-
* bigint, a Date,
|
|
325
|
-
* filter operand, a data value, a cursor
|
|
324
|
+
* bigint, a Date, an AvDecimal or a Uint8Array is encoded wherever it stands (a
|
|
325
|
+
* filter operand, a data value, a cursor), and plain objects and arrays are
|
|
326
326
|
* walked. Every other value is left for serializeWireBody, which refuses what is
|
|
327
327
|
* not JSON-safe. A json value cannot hold one of the four classes (the types
|
|
328
328
|
* exclude them), so nothing inside one is ever re-read as a scalar.
|
|
@@ -333,8 +333,8 @@ export function encodeArguments(value: unknown): unknown {
|
|
|
333
333
|
|
|
334
334
|
/**
|
|
335
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
|
|
337
|
-
* Proxy
|
|
336
|
+
* here and in the transaction builder, so a value that cannot be read (a revoked
|
|
337
|
+
* Proxy such as an Immer draft after produce, a throwing getter or trap) is refused at its
|
|
338
338
|
* path with a TypeError before anything is sent, never with the engine's own error.
|
|
339
339
|
*/
|
|
340
340
|
export function readArgument<T>(path: Path, read: () => T): T {
|
|
@@ -352,7 +352,7 @@ function objectKind(value: object): "datetime" | "decimal" | "bytes" | "containe
|
|
|
352
352
|
if (value instanceof Date) {
|
|
353
353
|
return "datetime";
|
|
354
354
|
}
|
|
355
|
-
if (value instanceof
|
|
355
|
+
if (value instanceof AvDecimal) {
|
|
356
356
|
return "decimal";
|
|
357
357
|
}
|
|
358
358
|
if (value instanceof Uint8Array) {
|
|
@@ -413,13 +413,13 @@ export type DecodeTable = {
|
|
|
413
413
|
* table: a number (a count), null, and the data of a Resource the table
|
|
414
414
|
* does not name pass through; a list is a list of
|
|
415
415
|
* records, anything else is one record of \`resource\`. In a record, a field the
|
|
416
|
-
* table names is revived
|
|
416
|
+
* table names is revived (each item of a list field), a relation recurses with
|
|
417
417
|
* its target, and every other key passes through untouched: json is never
|
|
418
418
|
* revived. A to-many relation's value is a list, \`{ data, count }\` or
|
|
419
|
-
* \`{ count }
|
|
419
|
+
* \`{ count }\`, the three shapes the derivation gives it.
|
|
420
420
|
*
|
|
421
|
-
* @throws TypeError when a value is not the wire form its field's scalar takes
|
|
422
|
-
* which the transport answers as
|
|
421
|
+
* @throws TypeError when a value is not the wire form its field's scalar takes,
|
|
422
|
+
* which the transport answers as an AvTransportError.
|
|
423
423
|
*/
|
|
424
424
|
export function decodeResult(table: DecodeTable, resource: string, value: unknown): unknown {
|
|
425
425
|
// A Resource the table does not name has nothing to revive: its data passes.
|
|
@@ -477,9 +477,9 @@ export function emitScalarCodecModule(style) {
|
|
|
477
477
|
path: "runtime/codec.ts",
|
|
478
478
|
source: codecModuleHead(moduleSpecifierWriter(style)) +
|
|
479
479
|
"\n/** The built-in scalars of the protocol. */\n" +
|
|
480
|
-
`export type
|
|
480
|
+
`export type AvBuiltInScalar =\n${union};\n` +
|
|
481
481
|
CODEC_MODULE_BODY +
|
|
482
|
-
"\nconst SCALAR_CODECS: { readonly [S in
|
|
482
|
+
"\nconst SCALAR_CODECS: { readonly [S in AvBuiltInScalar]: ScalarCodec } = {\n" +
|
|
483
483
|
`${table}\n};\n`,
|
|
484
484
|
};
|
|
485
485
|
}
|
|
@@ -24,8 +24,8 @@ export interface FingerprintNode {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* The node's fingerprint, or null when its arguments are not canonical JSON
|
|
28
|
-
* lone surrogate, a non-finite number
|
|
27
|
+
* The node's fingerprint, or null when its arguments are not canonical JSON (a
|
|
28
|
+
* lone surrogate, a non-finite number), exactly when core cannot compute one
|
|
29
29
|
* either; the server then answers the operation itself.
|
|
30
30
|
*/
|
|
31
31
|
export function fingerprintOf(node: FingerprintNode): string | null {
|
|
@@ -210,20 +210,20 @@ function base64Url(bytes: Uint8Array): string {
|
|
|
210
210
|
`;
|
|
211
211
|
function transactionModuleSource(from) {
|
|
212
212
|
return `import { encodeArguments, readArgument, serializeWireBody } from ${from("./codec")};
|
|
213
|
-
import { type
|
|
213
|
+
import { type AvCause, type AvError, frameworkErrorOf } from ${from("./errors")};
|
|
214
214
|
import { fingerprintOf } from ${from("./fingerprint")};
|
|
215
|
-
import { type
|
|
215
|
+
import { type AvCallOptions, executePlan, type TransportConnection } from ${from("./transport")};
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
218
|
* The deferred transaction builder and runner: \`avClient.tx\` defers an operation
|
|
219
|
-
* into a handle
|
|
219
|
+
* into a handle (pure data, no request), and \`avClient.transaction\` binds a
|
|
220
220
|
* list of handles into one transaction plan, sends it once, and resolves one
|
|
221
221
|
* result per handle in list order. A handle has no connection, so any client of
|
|
222
222
|
* this generated tree may run it.
|
|
223
223
|
*/
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
|
-
* What a handle defers
|
|
226
|
+
* What a handle defers, or why its arguments could not be read: the builder
|
|
227
227
|
* never throws into app code, and running the handle refuses it instead.
|
|
228
228
|
*/
|
|
229
229
|
interface DeferredRecord {
|
|
@@ -331,7 +331,7 @@ function placeholderSites(args: unknown): readonly PlaceholderSite[] {
|
|
|
331
331
|
/**
|
|
332
332
|
* Defers one operation into a handle: nothing is sent, and nothing is thrown.
|
|
333
333
|
* Its arguments are copied now, so a later change to the caller's data cannot
|
|
334
|
-
* move a reference
|
|
334
|
+
* move a reference, and a handle can reference only handles built before it.
|
|
335
335
|
* Arguments that cannot be read are kept as their refusal, which running the
|
|
336
336
|
* handle rejects with.
|
|
337
337
|
*/
|
|
@@ -360,8 +360,8 @@ export function deferOperation(resource: string, family: string, variant: string
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
/** What core answers in process for a plan entry it cannot run (A2004 / V1001). */
|
|
363
|
-
function entryRefusal(operation: number, message: string):
|
|
364
|
-
const cause:
|
|
363
|
+
function entryRefusal(operation: number, message: string): AvError {
|
|
364
|
+
const cause: AvCause = {
|
|
365
365
|
message: "Transaction plan failed framework validation.",
|
|
366
366
|
issues: [{ path: ["operations", operation], code: "V1001", message }],
|
|
367
367
|
operation,
|
|
@@ -369,19 +369,29 @@ function entryRefusal(operation: number, message: string): FrameworkError {
|
|
|
369
369
|
return frameworkErrorOf("A2004", cause);
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
/**
|
|
373
|
+
* The checks a plan's steps run before it is sent (the ClientContract's field
|
|
374
|
+
* validators, when it has any). Throws the AvError the server would answer.
|
|
375
|
+
*/
|
|
376
|
+
export type PlanCheck = (
|
|
377
|
+
steps: readonly { readonly resource: string; readonly family: string; readonly args: unknown }[],
|
|
378
|
+
) => void;
|
|
379
|
+
|
|
372
380
|
/**
|
|
373
381
|
* Binds \`steps\` into the plan, sends it once, and resolves one decoded result per
|
|
374
382
|
* step. Refused before anything is sent, with what core answers in
|
|
375
383
|
* process: a list that is not one, an entry that is not a handle of this
|
|
376
|
-
* generated client, one handle twice (
|
|
377
|
-
* ["operations", i]: the entry itself); a reference to a handle not in the list (
|
|
384
|
+
* generated client, one handle twice (AvValidationError A2004 / V1001 at
|
|
385
|
+
* ["operations", i]: the entry itself); a reference to a handle not in the list (AvValidationError A2007 /
|
|
378
386
|
* V1010 at its \`$ref\`); a handle whose arguments could not be read when it was
|
|
379
|
-
* built (the TypeError naming where)
|
|
387
|
+
* built (the TypeError naming where); and, when \`check\` is given, a step whose
|
|
388
|
+
* values break a field check (AvValidationError A2004, as the server answers it).
|
|
380
389
|
*/
|
|
381
390
|
export async function runTransaction(
|
|
382
391
|
connection: TransportConnection,
|
|
383
392
|
steps: unknown,
|
|
384
|
-
options:
|
|
393
|
+
options: AvCallOptions = {},
|
|
394
|
+
check?: PlanCheck,
|
|
385
395
|
): Promise<unknown[]> {
|
|
386
396
|
if (!Array.isArray(steps)) {
|
|
387
397
|
throw frameworkErrorOf("A2004", {
|
|
@@ -423,9 +433,11 @@ export async function runTransaction(
|
|
|
423
433
|
}
|
|
424
434
|
}
|
|
425
435
|
|
|
436
|
+
check?.(records);
|
|
437
|
+
|
|
426
438
|
// Bound lazily: a node's references need their source's
|
|
427
439
|
// fingerprint, so each node is built once, on first need, wherever its source
|
|
428
|
-
// stands in the list
|
|
440
|
+
// stands in the list. The server, not this client, refuses a later one.
|
|
429
441
|
const nodes = new Map<number, PlanNode>();
|
|
430
442
|
const nodeAt = (index: number): PlanNode => {
|
|
431
443
|
const built = nodes.get(index);
|
package/dist/generate.d.ts
CHANGED
|
@@ -73,8 +73,8 @@ export type ClientGenerationOutcome = ClientGenerated | ClientUpToDate | Foreign
|
|
|
73
73
|
* generator did not produce stands in its way and `overrideForeign` is not set,
|
|
74
74
|
* says what it is and writes nothing.
|
|
75
75
|
*
|
|
76
|
-
* @throws a refusal (`
|
|
77
|
-
* `
|
|
76
|
+
* @throws a refusal (`AvEnvFileError`, `AvClientConfigError`, `AvContractTransportError`,
|
|
77
|
+
* `AvContractProtocolError`, `AvGeneratedNameError`, `AvOutputWriteError`) whose
|
|
78
78
|
* message is the whole diagnosis; the previous output is intact. Anything else
|
|
79
79
|
* is a defect.
|
|
80
80
|
*/
|
package/dist/generate.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadClientContractSince } from "./contract/contract.loader.js";
|
|
|
9
9
|
import { emitClientTree } from "./emit/client-tree.emitter.js";
|
|
10
10
|
import { parseContractCarrier } from "./emit/contract-carrier.emitter.js";
|
|
11
11
|
import { CLIENT_ENTRY_FILE, CONTRACT_CARRIER_MODULE, GENERATED_DIRECTORY, } from "./emit/emitted-tree.interface.js";
|
|
12
|
-
import {
|
|
12
|
+
import { AvOutputWriteError, findForeignOutputContent, ownedOutputMatches, precedeDefect, writeClientOutput, } from "./output/output.writer.js";
|
|
13
13
|
const runPhase = async (_title, body) => await body();
|
|
14
14
|
async function storedContract(generateAt) {
|
|
15
15
|
try {
|
|
@@ -87,7 +87,7 @@ export async function generateClient(input) {
|
|
|
87
87
|
const into = path.relative(input.directory, config.generateAt) || ".";
|
|
88
88
|
const write = (overrideForeign) => step(`Checking and writing the client into ${into}`, () => writeInto(overrideForeign));
|
|
89
89
|
const foreign = await findForeignOutputContent(config.generateAt).catch((error) => {
|
|
90
|
-
throw error instanceof
|
|
90
|
+
throw error instanceof AvOutputWriteError
|
|
91
91
|
? error.precededBy(emission.warnings)
|
|
92
92
|
: precedeDefect(error, emission.warnings);
|
|
93
93
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** The version of `@aventara/client`. */
|
|
2
2
|
export declare const AVENTARA_CLIENT_GENERATOR_VERSION: string;
|
|
3
|
-
export type {
|
|
3
|
+
export type { AvClientConfigInput, AvClientConfigValue, AvEnvReference, } from "./config/client-config.interface.js";
|
|
4
4
|
export { defineClientConfig, env } from "./config/config.resolver.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** `avclient init`'s refusals after the questions: each one sentence, exit 1. */
|
|
2
2
|
/** Nobody confirmed replacing existing content. A refusal: nothing was touched. */
|
|
3
|
-
export declare class
|
|
4
|
-
readonly name = "
|
|
3
|
+
export declare class AvClientInitNotConfirmedError extends Error {
|
|
4
|
+
readonly name = "AvClientInitNotConfirmedError";
|
|
5
5
|
}
|
|
6
6
|
/** A step after the write failed. Reported with what to run; the files are kept. */
|
|
7
|
-
export declare class
|
|
8
|
-
readonly name = "
|
|
7
|
+
export declare class AvClientInitStepError extends Error {
|
|
8
|
+
readonly name = "AvClientInitStepError";
|
|
9
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export class
|
|
2
|
-
name = "
|
|
1
|
+
export class AvClientInitNotConfirmedError extends Error {
|
|
2
|
+
name = "AvClientInitNotConfirmedError";
|
|
3
3
|
}
|
|
4
|
-
export class
|
|
5
|
-
name = "
|
|
4
|
+
export class AvClientInitStepError extends Error {
|
|
5
|
+
name = "AvClientInitStepError";
|
|
6
6
|
}
|
|
@@ -4,9 +4,9 @@ import { GENERATION_STEPS, runGenerate, } from "../cli/generate.command.js";
|
|
|
4
4
|
import { createStepReporter } from "../cli/step.reporter.js";
|
|
5
5
|
import { CLIENT_CONFIG_FILE } from "../config/config.loader.js";
|
|
6
6
|
import { resolveModuleStyle } from "../config/module-style.resolver.js";
|
|
7
|
-
import {
|
|
7
|
+
import { AvContractTransportError } from "../contract/contract.fetcher.js";
|
|
8
8
|
import { CLIENT_ENTRY_FILE } from "../emit/emitted-tree.interface.js";
|
|
9
|
-
import {
|
|
9
|
+
import { AvClientInitNotConfirmedError, AvClientInitStepError, } from "./client-init.errors.js";
|
|
10
10
|
import { GENERATE_SCRIPT, planClientInit } from "./client-init.planner.js";
|
|
11
11
|
import { resolveClientInitAnswers } from "./client-init.questions.js";
|
|
12
12
|
import { inspectClientProject } from "./client-project.inspector.js";
|
|
@@ -40,11 +40,11 @@ export async function runClientInit(command, io) {
|
|
|
40
40
|
const them = plan.conflicts.length === 1 ? "it" : "them";
|
|
41
41
|
const has = plan.conflicts.length === 1 ? "has" : "have";
|
|
42
42
|
if (!io.interactive) {
|
|
43
|
-
throw new
|
|
43
|
+
throw new AvClientInitNotConfirmedError(`not initializing, and nothing was touched: ${plan.conflicts.join(", ")} already ${has} other content and nobody can be asked (stdin is not a terminal); run \`avclient init --yes\` to replace ${them}.`);
|
|
44
44
|
}
|
|
45
45
|
io.stderr(`avclient: warning: ${plan.conflicts.join(", ")} already ${has} other content, and initializing will replace ${them}.\n`);
|
|
46
46
|
if (!(await io.confirm("avclient: replace them and initialize? [y/N] "))) {
|
|
47
|
-
throw new
|
|
47
|
+
throw new AvClientInitNotConfirmedError(`cancelled, and nothing was touched; run again and answer yes to replace ${them}.`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
const writes = plan.conflicts.length === 0 ? plan.keeping : plan.replacing;
|
|
@@ -80,7 +80,7 @@ export async function runClientInit(command, io) {
|
|
|
80
80
|
onOutput: progress.output,
|
|
81
81
|
});
|
|
82
82
|
if (installed.code !== 0) {
|
|
83
|
-
throw new
|
|
83
|
+
throw new AvClientInitStepError(`\`${install}\` exited with code ${installed.code} after ${project.configFile ?? CLIENT_CONFIG_FILE} was written; fix what it reports and run \`${install}\` again:\n${installed.stderr.trimEnd()}`);
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
86
|
if (command.skipGenerate) {
|
|
@@ -92,8 +92,8 @@ export async function runClientInit(command, io) {
|
|
|
92
92
|
return await runGenerate(io, command.yes, progress);
|
|
93
93
|
}
|
|
94
94
|
catch (error) {
|
|
95
|
-
if (error instanceof
|
|
96
|
-
throw new
|
|
95
|
+
if (error instanceof AvContractTransportError) {
|
|
96
|
+
throw new AvClientInitStepError(`the project is set up, but the server did not answer (${error.message.replace(/\.$/, "")}); once it is running, run \`avclient generate\`.`);
|
|
97
97
|
}
|
|
98
98
|
throw error;
|
|
99
99
|
}
|
|
@@ -8,12 +8,12 @@ export type ClientInitAnswers = {
|
|
|
8
8
|
};
|
|
9
9
|
type QuestionId = "entrypoint" | "envVar" | "generateAt" | "packageManager";
|
|
10
10
|
/** An answer the question cannot take. A refusal: one sentence. */
|
|
11
|
-
export declare class
|
|
12
|
-
readonly name = "
|
|
11
|
+
export declare class AvClientInitAnswerError extends Error {
|
|
12
|
+
readonly name = "AvClientInitAnswerError";
|
|
13
13
|
}
|
|
14
14
|
/** Nobody can answer and some questions are unanswered. A refusal: one sentence. */
|
|
15
|
-
export declare class
|
|
16
|
-
readonly name = "
|
|
15
|
+
export declare class AvClientInitUnansweredError extends Error {
|
|
16
|
+
readonly name = "AvClientInitUnansweredError";
|
|
17
17
|
}
|
|
18
18
|
export type ClientInitQuestion = {
|
|
19
19
|
readonly id: QuestionId;
|
|
@@ -21,7 +21,7 @@ export type ClientInitQuestion = {
|
|
|
21
21
|
readonly placeholder: string;
|
|
22
22
|
readonly label: string;
|
|
23
23
|
readonly defaultFor: (detectedPackageManager: PackageManager) => string;
|
|
24
|
-
/** @throws
|
|
24
|
+
/** @throws AvClientInitAnswerError */
|
|
25
25
|
readonly parse: (raw: string) => string;
|
|
26
26
|
};
|
|
27
27
|
export declare const DEFAULT_ENV_VAR = "AVENTARA_API_URL";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { resolveEntrypoint } from "../config/config.resolver.js";
|
|
2
2
|
export const PACKAGE_MANAGERS = ["npm", "pnpm"];
|
|
3
|
-
export class
|
|
4
|
-
name = "
|
|
3
|
+
export class AvClientInitAnswerError extends Error {
|
|
4
|
+
name = "AvClientInitAnswerError";
|
|
5
5
|
}
|
|
6
|
-
export class
|
|
7
|
-
name = "
|
|
6
|
+
export class AvClientInitUnansweredError extends Error {
|
|
7
|
+
name = "AvClientInitUnansweredError";
|
|
8
8
|
}
|
|
9
9
|
export const DEFAULT_ENV_VAR = "AVENTARA_API_URL";
|
|
10
10
|
export const CLIENT_INIT_QUESTIONS = [
|
|
@@ -19,7 +19,7 @@ export const CLIENT_INIT_QUESTIONS = [
|
|
|
19
19
|
resolveEntrypoint(raw.trim());
|
|
20
20
|
}
|
|
21
21
|
catch (error) {
|
|
22
|
-
throw new
|
|
22
|
+
throw new AvClientInitAnswerError(error.message.replace(/\.$/, ""));
|
|
23
23
|
}
|
|
24
24
|
return raw.trim();
|
|
25
25
|
},
|
|
@@ -33,7 +33,7 @@ export const CLIENT_INIT_QUESTIONS = [
|
|
|
33
33
|
parse: (raw) => {
|
|
34
34
|
const name = raw.trim();
|
|
35
35
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
36
|
-
throw new
|
|
36
|
+
throw new AvClientInitAnswerError(`${JSON.stringify(raw)} is not an environment variable name.`);
|
|
37
37
|
}
|
|
38
38
|
return name;
|
|
39
39
|
},
|
|
@@ -47,7 +47,7 @@ export const CLIENT_INIT_QUESTIONS = [
|
|
|
47
47
|
parse: (raw) => {
|
|
48
48
|
const directory = raw.trim();
|
|
49
49
|
if (directory === "") {
|
|
50
|
-
throw new
|
|
50
|
+
throw new AvClientInitAnswerError("--generate-at takes a directory.");
|
|
51
51
|
}
|
|
52
52
|
return directory;
|
|
53
53
|
},
|
|
@@ -61,7 +61,7 @@ export const CLIENT_INIT_QUESTIONS = [
|
|
|
61
61
|
parse: (raw) => {
|
|
62
62
|
const manager = PACKAGE_MANAGERS.find((candidate) => candidate === raw.trim());
|
|
63
63
|
if (manager === undefined) {
|
|
64
|
-
throw new
|
|
64
|
+
throw new AvClientInitAnswerError(`${JSON.stringify(raw)} is not a supported package manager; choose ${PACKAGE_MANAGERS.join(" or ")}.`);
|
|
65
65
|
}
|
|
66
66
|
return manager;
|
|
67
67
|
},
|
|
@@ -90,7 +90,7 @@ export async function resolveClientInitAnswers(sources) {
|
|
|
90
90
|
break;
|
|
91
91
|
}
|
|
92
92
|
catch (error) {
|
|
93
|
-
if (!(error instanceof
|
|
93
|
+
if (!(error instanceof AvClientInitAnswerError)) {
|
|
94
94
|
throw error;
|
|
95
95
|
}
|
|
96
96
|
sources.say(error.message);
|
|
@@ -102,7 +102,7 @@ export async function resolveClientInitAnswers(sources) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
if (missing.length > 0) {
|
|
105
|
-
throw new
|
|
105
|
+
throw new AvClientInitUnansweredError(`stdin is not a terminal, so nothing can be asked: pass ${missing.join(", ")}, or pass --yes to accept their defaults.`);
|
|
106
106
|
}
|
|
107
107
|
return {
|
|
108
108
|
entrypoint: answered.entrypoint,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { PackageManager } from "./client-init.questions.js";
|
|
2
2
|
/** The frontend `avclient init` serves, read before anything is asked or written. */
|
|
3
3
|
/** The directory is not a project `avclient init` can set up. A refusal: nothing written. */
|
|
4
|
-
export declare class
|
|
5
|
-
readonly name = "
|
|
4
|
+
export declare class AvClientProjectRefusedError extends Error {
|
|
5
|
+
readonly name = "AvClientProjectRefusedError";
|
|
6
6
|
}
|
|
7
7
|
export type ClientProject = {
|
|
8
8
|
readonly directory: string;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { findClientConfigFile } from "../config/config.loader.js";
|
|
4
|
-
export class
|
|
5
|
-
name = "
|
|
4
|
+
export class AvClientProjectRefusedError extends Error {
|
|
5
|
+
name = "AvClientProjectRefusedError";
|
|
6
6
|
}
|
|
7
7
|
export function inspectClientProject(directory) {
|
|
8
8
|
const manifest = path.join(directory, "package.json");
|
|
9
9
|
if (!existsSync(manifest)) {
|
|
10
|
-
throw new
|
|
10
|
+
throw new AvClientProjectRefusedError("avclient init runs in a frontend project: no package.json here; create one first.");
|
|
11
11
|
}
|
|
12
12
|
const lockfiles = [
|
|
13
13
|
["package-lock.json", "npm"],
|
|
@@ -5,9 +5,9 @@ import { type OutputCheck, type TypeScriptResolver } from "./output.validator.js
|
|
|
5
5
|
/** The four stages a write passes through, in order. */
|
|
6
6
|
export type OutputWriteStage = "prepare" | "temp" | "validate" | "replace";
|
|
7
7
|
/** A write that stopped; the previous output is intact unless it says otherwise. */
|
|
8
|
-
export declare class
|
|
8
|
+
export declare class AvOutputWriteError extends Error {
|
|
9
9
|
readonly stage: OutputWriteStage;
|
|
10
|
-
readonly name = "
|
|
10
|
+
readonly name = "AvOutputWriteError";
|
|
11
11
|
/**
|
|
12
12
|
* What the run said before it stopped, in the order a success returns its
|
|
13
13
|
* warnings: the emission's, the validator's, the writer's. Printed before the
|
|
@@ -19,7 +19,7 @@ export declare class OutputWriteError extends Error {
|
|
|
19
19
|
readonly warnings?: readonly string[];
|
|
20
20
|
});
|
|
21
21
|
/** The same refusal, with `earlier` said before the warnings it already carries. */
|
|
22
|
-
precededBy(earlier: readonly string[]):
|
|
22
|
+
precededBy(earlier: readonly string[]): AvOutputWriteError;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Records `earlier` as said before `defect`, ahead of anything already recorded,
|
|
@@ -49,11 +49,11 @@ export interface ClientOutputWritten {
|
|
|
49
49
|
* Writes `emission.tree` as `<generateAt>/AvClient.ts` and
|
|
50
50
|
* `<generateAt>/generated/`.
|
|
51
51
|
*
|
|
52
|
-
* @throws
|
|
52
|
+
* @throws AvOutputWriteError when the write stops; the previous output is intact.
|
|
53
53
|
*/
|
|
54
54
|
export declare function writeClientOutput(input: ClientOutputWriteInput): Promise<ClientOutputWritten>;
|
|
55
55
|
/**
|
|
56
|
-
* @throws
|
|
56
|
+
* @throws AvOutputWriteError when `generateAt` is not a directory, `generated` is
|
|
57
57
|
* not a directory or an entry file is not a file: no confirmation repairs that.
|
|
58
58
|
*/
|
|
59
59
|
export declare function findForeignOutputContent(generateAt: AbsolutePath, fileSystem?: OutputFileSystem): Promise<readonly string[]>;
|
|
@@ -4,9 +4,9 @@ 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
6
|
import { projectTypeScriptResolver, validateOutputTree, } from "./output.validator.js";
|
|
7
|
-
export class
|
|
7
|
+
export class AvOutputWriteError extends Error {
|
|
8
8
|
stage;
|
|
9
|
-
name = "
|
|
9
|
+
name = "AvOutputWriteError";
|
|
10
10
|
warnings;
|
|
11
11
|
constructor(stage, message, options) {
|
|
12
12
|
super(message, options);
|
|
@@ -14,7 +14,7 @@ export class OutputWriteError extends Error {
|
|
|
14
14
|
this.warnings = options?.warnings ?? [];
|
|
15
15
|
}
|
|
16
16
|
precededBy(earlier) {
|
|
17
|
-
return new
|
|
17
|
+
return new AvOutputWriteError(this.stage, this.message, {
|
|
18
18
|
cause: this.cause,
|
|
19
19
|
warnings: [...earlier, ...this.warnings],
|
|
20
20
|
});
|
|
@@ -65,7 +65,7 @@ export async function writeClientOutput(input) {
|
|
|
65
65
|
...validationWarnings,
|
|
66
66
|
...writerWarnings,
|
|
67
67
|
];
|
|
68
|
-
throw error instanceof
|
|
68
|
+
throw error instanceof AvOutputWriteError
|
|
69
69
|
? error.precededBy(said)
|
|
70
70
|
: precedeDefect(error, said);
|
|
71
71
|
}
|
|
@@ -99,7 +99,7 @@ export async function writeClientOutput(input) {
|
|
|
99
99
|
validationWarnings = validation.warnings;
|
|
100
100
|
}
|
|
101
101
|
else {
|
|
102
|
-
throw new
|
|
102
|
+
throw new AvOutputWriteError("validate", `the generated output failed its ${validation.checked} check, so ${LEFT_AS_THEY_WERE(paths)} ` +
|
|
103
103
|
"This is a defect in @aventara/client; please report it with these findings:\n" +
|
|
104
104
|
validation.findings.map((finding) => ` ${finding}`).join("\n"));
|
|
105
105
|
}
|
|
@@ -267,7 +267,7 @@ async function inspectOwnedEntries(fs, paths, generatedSource = paths.generated)
|
|
|
267
267
|
const generatedStats = await lstatOrUndefined(fs, generatedSource);
|
|
268
268
|
if (generatedStats !== undefined) {
|
|
269
269
|
if (!generatedStats.isDirectory()) {
|
|
270
|
-
throw new
|
|
270
|
+
throw new AvOutputWriteError("prepare", `refusing to replace ${generatedSource}: it is not a directory, and the generator owns ` +
|
|
271
271
|
`${GENERATED_DIRECTORY}/ as one; move it, or choose another \`generateAt\`. Nothing was written.`);
|
|
272
272
|
}
|
|
273
273
|
const entries = await fs.readdir(generatedSource, {
|
|
@@ -292,7 +292,7 @@ async function inspectOwnedEntries(fs, paths, generatedSource = paths.generated)
|
|
|
292
292
|
continue;
|
|
293
293
|
}
|
|
294
294
|
if (!stats.isFile()) {
|
|
295
|
-
throw new
|
|
295
|
+
throw new AvOutputWriteError("prepare", `refusing to replace ${file}: it is not a file, and the generator owns ${name} ` +
|
|
296
296
|
"as one; move it, or choose another `generateAt`. Nothing was written.");
|
|
297
297
|
}
|
|
298
298
|
entries.push(name);
|
|
@@ -317,12 +317,12 @@ async function inspectOwnedEntries(fs, paths, generatedSource = paths.generated)
|
|
|
317
317
|
}
|
|
318
318
|
function foreignContentRefusal(paths, foreign) {
|
|
319
319
|
const them = foreign.length === 1 ? "it" : "them";
|
|
320
|
-
return new
|
|
320
|
+
return new AvOutputWriteError("prepare", `refusing to generate into ${paths.generateAt}: ${foreign.join(", ")} ${foreign.length === 1 ? "was" : "were"} ` +
|
|
321
321
|
`not generated by @aventara/client, and generating would overwrite or remove ${them}; confirm the ` +
|
|
322
322
|
`override, move ${them} out, or choose another \`generateAt\`. Nothing was written.`);
|
|
323
323
|
}
|
|
324
324
|
function notADirectory(generateAt) {
|
|
325
|
-
return new
|
|
325
|
+
return new AvOutputWriteError("prepare", `refusing to generate into ${generateAt}: it is not a directory. ` +
|
|
326
326
|
"`generateAt` must name a directory; choose another `generateAt`. Nothing was written.");
|
|
327
327
|
}
|
|
328
328
|
async function carriesOwnershipLine(fs, file) {
|
|
@@ -341,7 +341,7 @@ async function rollBack(paths, cause, undo) {
|
|
|
341
341
|
await undo();
|
|
342
342
|
}
|
|
343
343
|
catch (rollbackError) {
|
|
344
|
-
throw new
|
|
344
|
+
throw new AvOutputWriteError("replace", `the generated output could not be moved into ${paths.generateAt} (${describe(cause)}), and the ` +
|
|
345
345
|
`previous output could not be restored (${describe(rollbackError)}). The previous ` +
|
|
346
346
|
`${GENERATED_DIRECTORY}/ is intact at ${paths.previous}; the next run puts it back.`, { cause });
|
|
347
347
|
}
|
|
@@ -351,10 +351,10 @@ async function stage(name, paths, run) {
|
|
|
351
351
|
return await run();
|
|
352
352
|
}
|
|
353
353
|
catch (error) {
|
|
354
|
-
if (error instanceof
|
|
354
|
+
if (error instanceof AvOutputWriteError || !isSystemError(error)) {
|
|
355
355
|
throw error;
|
|
356
356
|
}
|
|
357
|
-
throw new
|
|
357
|
+
throw new AvOutputWriteError(name, `could not ${STAGE_ACTIONS[name]} (${describe(error)}); ${LEFT_AS_THEY_WERE(paths)}`, { cause: error });
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
const STAGE_ACTIONS = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/client",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "Development-time generator for Aventara typed remote clients.",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"CHANGELOG.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@aventara/core": "1.0.0-rc.
|
|
27
|
+
"@aventara/core": "1.0.0-rc.1",
|
|
28
28
|
"c12": "3.3.4",
|
|
29
29
|
"get-tsconfig": "4.10.0"
|
|
30
30
|
},
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@aventara/testing": "1.0.0-rc.
|
|
43
|
+
"@aventara/testing": "1.0.0-rc.1",
|
|
44
44
|
"@types/node": "24.10.1",
|
|
45
45
|
"typescript": "^5.9.2"
|
|
46
46
|
},
|