@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.
- package/CHANGELOG.md +679 -0
- package/README.md +168 -132
- package/dist/cli/command.parser.d.ts +6 -4
- package/dist/cli/command.parser.js +22 -11
- package/dist/cli/generate.command.d.ts +6 -1
- package/dist/cli/generate.command.js +12 -1
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/cli/step.reporter.d.ts +31 -0
- package/dist/cli/step.reporter.js +87 -0
- package/dist/cli.js +2 -0
- 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 +77 -43
- 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/contract-carrier.emitter.js +1 -1
- package/dist/emit/derivation.emitter.js +1 -0
- package/dist/emit/descriptor.emitter.d.ts +2 -2
- package/dist/emit/descriptor.emitter.js +2 -2
- 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 +16 -0
- package/dist/emit/json-shape.emitter.js +88 -0
- package/dist/emit/name.deriver.d.ts +6 -6
- package/dist/emit/name.deriver.js +15 -15
- 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 +117 -112
- package/dist/emit/scalar.codec.d.ts +1 -1
- package/dist/emit/scalar.codec.js +78 -35
- package/dist/emit/transaction.emitter.js +131 -83
- package/dist/generate.d.ts +11 -3
- package/dist/generate.js +40 -26
- 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 +37 -22
- 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/init/command.runner.d.ts +2 -0
- package/dist/init/command.runner.js +4 -1
- package/dist/output/output.writer.d.ts +5 -5
- package/dist/output/output.writer.js +12 -12
- package/package.json +6 -5
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
|
-
import {
|
|
12
|
+
import { AvOutputWriteError, findForeignOutputContent, 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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 =
|
|
45
|
-
|
|
46
|
-
|
|
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 (
|
|
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
|
|
69
|
+
const writeInto = async (overrideForeign) => {
|
|
58
70
|
const written = await writeClientOutput(input.resolveTypeScript === undefined
|
|
59
71
|
? { emission, generateAt: config.generateAt, overrideForeign }
|
|
60
72
|
: {
|
|
@@ -72,8 +84,10 @@ 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
|
-
throw error instanceof
|
|
90
|
+
throw error instanceof AvOutputWriteError
|
|
77
91
|
? error.precededBy(emission.warnings)
|
|
78
92
|
: precedeDefect(error, emission.warnings);
|
|
79
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
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { runGenerate } from "../cli/generate.command.js";
|
|
3
|
+
import { GENERATION_STEPS, runGenerate, } from "../cli/generate.command.js";
|
|
4
|
+
import { createStepReporter } from "../cli/step.reporter.js";
|
|
4
5
|
import { CLIENT_CONFIG_FILE } from "../config/config.loader.js";
|
|
5
6
|
import { resolveModuleStyle } from "../config/module-style.resolver.js";
|
|
6
|
-
import {
|
|
7
|
+
import { AvContractTransportError } from "../contract/contract.fetcher.js";
|
|
7
8
|
import { CLIENT_ENTRY_FILE } from "../emit/emitted-tree.interface.js";
|
|
8
|
-
import {
|
|
9
|
+
import { AvClientInitNotConfirmedError, AvClientInitStepError, } from "./client-init.errors.js";
|
|
9
10
|
import { GENERATE_SCRIPT, planClientInit } from "./client-init.planner.js";
|
|
10
11
|
import { resolveClientInitAnswers } from "./client-init.questions.js";
|
|
11
12
|
import { inspectClientProject } from "./client-project.inspector.js";
|
|
@@ -39,46 +40,60 @@ export async function runClientInit(command, io) {
|
|
|
39
40
|
const them = plan.conflicts.length === 1 ? "it" : "them";
|
|
40
41
|
const has = plan.conflicts.length === 1 ? "has" : "have";
|
|
41
42
|
if (!io.interactive) {
|
|
42
|
-
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}.`);
|
|
43
44
|
}
|
|
44
45
|
io.stderr(`avclient: warning: ${plan.conflicts.join(", ")} already ${has} other content, and initializing will replace ${them}.\n`);
|
|
45
46
|
if (!(await io.confirm("avclient: replace them and initialize? [y/N] "))) {
|
|
46
|
-
throw new
|
|
47
|
+
throw new AvClientInitNotConfirmedError(`cancelled, and nothing was touched; run again and answer yes to replace ${them}.`);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
const writes = plan.conflicts.length === 0 ? plan.keeping : plan.replacing;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const progress = createStepReporter({
|
|
52
|
+
write: io.stderr,
|
|
53
|
+
mode: io.progress ?? "off",
|
|
54
|
+
total: command.skipInstall
|
|
55
|
+
? 1
|
|
56
|
+
: 2 + (command.skipGenerate ? 0 : GENERATION_STEPS),
|
|
57
|
+
verbose: command.verbose === true,
|
|
58
|
+
});
|
|
59
|
+
await progress.step("Writing the client setup", () => {
|
|
60
|
+
for (const write of writes) {
|
|
61
|
+
const target = path.join(io.cwd, write.path);
|
|
62
|
+
if (write.content === undefined) {
|
|
63
|
+
rmSync(target, { force: true });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
67
|
+
writeFileSync(target, write.content);
|
|
55
68
|
}
|
|
56
|
-
|
|
57
|
-
writeFileSync(target, write.content);
|
|
58
|
-
}
|
|
69
|
+
});
|
|
59
70
|
io.stdout(`avclient: initialized ${io.cwd}: ${writes.map((write) => (write.content === undefined ? `removed ${write.path}` : write.path)).join(", ") || "nothing to change"}.\n`);
|
|
60
71
|
const install = `${answers.packageManager} install`;
|
|
61
72
|
if (command.skipInstall) {
|
|
73
|
+
progress.finish("avclient", "done");
|
|
62
74
|
io.stdout(`Next:\n 1. Install @aventara/client ${clientVersion}: ${install}\n 2. With the server running: ${answers.packageManager} run ${GENERATE_SCRIPT}\n`);
|
|
63
75
|
return 0;
|
|
64
76
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
await progress.step(`Installing dependencies (${install})`, async () => {
|
|
78
|
+
const installed = await (io.run ?? runCommand)(answers.packageManager, ["install"], {
|
|
79
|
+
cwd: io.cwd,
|
|
80
|
+
onOutput: progress.output,
|
|
81
|
+
});
|
|
82
|
+
if (installed.code !== 0) {
|
|
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
|
+
}
|
|
68
85
|
});
|
|
69
|
-
if (installed.code !== 0) {
|
|
70
|
-
throw new ClientInitStepError(`\`${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()}`);
|
|
71
|
-
}
|
|
72
86
|
if (command.skipGenerate) {
|
|
87
|
+
progress.finish("avclient", "done");
|
|
73
88
|
io.stdout(`Next: with the server running, ${answers.packageManager} run ${GENERATE_SCRIPT}\n`);
|
|
74
89
|
return 0;
|
|
75
90
|
}
|
|
76
91
|
try {
|
|
77
|
-
return await runGenerate(io, command.yes);
|
|
92
|
+
return await runGenerate(io, command.yes, progress);
|
|
78
93
|
}
|
|
79
94
|
catch (error) {
|
|
80
|
-
if (error instanceof
|
|
81
|
-
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\`.`);
|
|
82
97
|
}
|
|
83
98
|
throw error;
|
|
84
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"],
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** One child process, stdout and stderr piped and collected: `init`'s install. */
|
|
2
2
|
export type CommandRunner = (command: string, args: readonly string[], options: {
|
|
3
3
|
readonly cwd: string;
|
|
4
|
+
/** Each chunk of the child's stdout and stderr as it arrives (`--verbose`). */
|
|
5
|
+
readonly onOutput?: (chunk: string) => void;
|
|
4
6
|
}) => Promise<{
|
|
5
7
|
readonly code: number;
|
|
6
8
|
readonly stderr: string;
|
|
@@ -6,9 +6,12 @@ export const runCommand = (command, args, options) => new Promise((resolve, reje
|
|
|
6
6
|
env: process.env,
|
|
7
7
|
});
|
|
8
8
|
let stderr = "";
|
|
9
|
-
child.stdout.
|
|
9
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
10
|
+
options.onOutput?.(chunk);
|
|
11
|
+
});
|
|
10
12
|
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
11
13
|
stderr += chunk;
|
|
14
|
+
options.onOutput?.(chunk);
|
|
12
15
|
});
|
|
13
16
|
child.once("error", reject);
|
|
14
17
|
child.once("close", (code) => {
|
|
@@ -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": "
|
|
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",
|
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
|
-
"LICENSE-ADDITIONAL-PERMISSION.md"
|
|
23
|
+
"LICENSE-ADDITIONAL-PERMISSION.md",
|
|
24
|
+
"CHANGELOG.md"
|
|
24
25
|
],
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@aventara/core": "
|
|
27
|
+
"@aventara/core": "1.0.0-rc.1",
|
|
27
28
|
"c12": "3.3.4",
|
|
28
29
|
"get-tsconfig": "4.10.0"
|
|
29
30
|
},
|
|
@@ -39,12 +40,12 @@
|
|
|
39
40
|
"access": "public"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@aventara/testing": "
|
|
43
|
+
"@aventara/testing": "1.0.0-rc.1",
|
|
43
44
|
"@types/node": "24.10.1",
|
|
44
45
|
"typescript": "^5.9.2"
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
|
-
"build": "tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist",
|
|
48
|
+
"build": "node ../../scripts/build/build-output.cleaner.ts dist && tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist",
|
|
48
49
|
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
|
|
49
50
|
"test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
|
|
50
51
|
"lint": "biome lint src"
|