@aventara/client 1.0.0-rc.0 → 1.0.0-rc.10
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 +651 -0
- package/README.md +195 -141
- package/dist/cli/command.parser.d.ts +7 -6
- package/dist/cli/command.parser.js +13 -13
- package/dist/cli/generation-failure.renderer.d.ts +2 -1
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/cli/generation-success.renderer.d.ts +2 -1
- package/dist/cli/step.reporter.d.ts +2 -1
- package/dist/cli/terminal.prompter.d.ts +2 -1
- package/dist/config/client-config.interface.d.ts +24 -7
- package/dist/config/config.loader.d.ts +12 -7
- package/dist/config/config.loader.js +26 -8
- package/dist/config/config.resolver.d.ts +16 -9
- package/dist/config/config.resolver.js +40 -12
- package/dist/config/env.cascade.d.ts +4 -8
- package/dist/config/env.cascade.js +7 -7
- package/dist/config/module-style.resolver.d.ts +4 -7
- package/dist/config/module-style.resolver.js +4 -4
- package/dist/config/tsconfig.locator.d.ts +3 -2
- package/dist/config/tsconfig.locator.js +16 -4
- package/dist/contract/contract.acceptance.d.ts +4 -5
- package/dist/contract/contract.acceptance.js +5 -5
- package/dist/contract/contract.fetcher.d.ts +5 -4
- package/dist/contract/contract.fetcher.js +6 -6
- package/dist/contract/contract.loader.d.ts +9 -8
- package/dist/contract/contract.loader.js +2 -2
- package/dist/emit/access.emitter.d.ts +12 -0
- package/dist/emit/access.emitter.js +263 -0
- package/dist/emit/banner.emitter.d.ts +6 -1
- package/dist/emit/banner.emitter.js +12 -8
- package/dist/emit/client-surface.emitter.d.ts +2 -2
- package/dist/emit/client-surface.emitter.js +136 -41
- package/dist/emit/client-tree.emitter.d.ts +11 -4
- package/dist/emit/client-tree.emitter.js +51 -17
- package/dist/emit/contract-carrier.emitter.d.ts +3 -3
- package/dist/emit/derivation.emitter.d.ts +3 -3
- package/dist/emit/derivation.emitter.js +7 -3
- 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 +22 -0
- package/dist/emit/field-check.emitter.js +592 -0
- package/dist/emit/json-shape.emitter.d.ts +2 -2
- package/dist/emit/module-specifier.scanner.d.ts +2 -1
- package/dist/emit/name.deriver.d.ts +8 -9
- package/dist/emit/name.deriver.js +18 -15
- package/dist/emit/named-type.emitter.d.ts +2 -2
- package/dist/emit/named-type.emitter.js +12 -2
- package/dist/emit/runtime.emitter.d.ts +6 -5
- package/dist/emit/runtime.emitter.js +215 -116
- package/dist/emit/scalar.codec.d.ts +1 -1
- package/dist/emit/scalar.codec.js +246 -29
- package/dist/emit/transaction.emitter.js +30 -14
- package/dist/generate.d.ts +5 -4
- package/dist/generate.js +3 -3
- package/dist/hydration/hydration-tag.format.d.ts +11 -0
- package/dist/hydration/hydration-tag.format.js +6 -0
- 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.planner.d.ts +3 -2
- package/dist/init/client-init.questions.d.ts +5 -16
- package/dist/init/client-init.questions.js +12 -12
- package/dist/init/client-project.inspector.d.ts +2 -2
- package/dist/init/client-project.inspector.js +3 -3
- package/dist/internal.d.ts +25 -0
- package/dist/internal.js +11 -0
- package/dist/output/output.validator.d.ts +3 -4
- package/dist/output/output.validator.js +2 -2
- package/dist/output/output.writer.d.ts +8 -7
- package/dist/output/output.writer.js +12 -12
- package/package.json +9 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
interface GenerationFailureReport {
|
|
2
2
|
readonly exitCode: number;
|
|
3
3
|
/** What goes to stderr, newline-terminated: the warnings carried, then the sentence. */
|
|
4
4
|
readonly text: string;
|
|
5
5
|
}
|
|
6
6
|
export declare function renderGenerationFailure(error: unknown): GenerationFailureReport;
|
|
7
|
+
export {};
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
1
|
+
import { AvClientConfigError } from "../config/config.resolver.js";
|
|
2
|
+
import { AvEnvFileError } from "../config/env.cascade.js";
|
|
3
|
+
import { AvContractProtocolError } from "../contract/contract.acceptance.js";
|
|
4
|
+
import { AvContractTransportError } from "../contract/contract.fetcher.js";
|
|
5
|
+
import { AvGeneratedNameError } from "../emit/name.deriver.js";
|
|
6
|
+
import { AvClientInitNotConfirmedError, AvClientInitStepError, } from "../init/client-init.errors.js";
|
|
7
|
+
import { AvClientInitAnswerError, AvClientInitUnansweredError, } from "../init/client-init.questions.js";
|
|
8
|
+
import { AvClientProjectRefusedError } from "../init/client-project.inspector.js";
|
|
9
|
+
import { AvOutputWriteError, warningsRaisedBeforeDefect, } from "../output/output.writer.js";
|
|
10
|
+
import { AvCliCommandError } from "./command.parser.js";
|
|
11
11
|
import { renderWarningLines } from "./warning.renderer.js";
|
|
12
12
|
const REFUSALS = [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
AvCliCommandError,
|
|
14
|
+
AvClientConfigError,
|
|
15
|
+
AvEnvFileError,
|
|
16
|
+
AvContractTransportError,
|
|
17
|
+
AvContractProtocolError,
|
|
18
|
+
AvGeneratedNameError,
|
|
19
|
+
AvOutputWriteError,
|
|
20
|
+
AvClientInitAnswerError,
|
|
21
|
+
AvClientInitUnansweredError,
|
|
22
|
+
AvClientProjectRefusedError,
|
|
23
|
+
AvClientInitNotConfirmedError,
|
|
24
|
+
AvClientInitStepError,
|
|
25
25
|
];
|
|
26
26
|
export function renderGenerationFailure(error) {
|
|
27
27
|
if (REFUSALS.some((refusal) => error instanceof refusal)) {
|
|
28
|
-
const carried = error instanceof
|
|
28
|
+
const carried = error instanceof AvOutputWriteError ? error.warnings : [];
|
|
29
29
|
return {
|
|
30
|
-
exitCode: error instanceof
|
|
30
|
+
exitCode: error instanceof AvContractProtocolError ? error.exitCode : 1,
|
|
31
31
|
text: `${renderWarningLines(carried)}avclient: ${error.message}\n`,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
@@ -6,7 +6,7 @@ import type { ClientGenerated, ClientUpToDate, ForeignOutputContent } from "../g
|
|
|
6
6
|
* them — renames, the degraded check, crash recovery — so the same run prints the
|
|
7
7
|
* same lines.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
interface GenerationSuccessReport {
|
|
10
10
|
/** What goes to stdout, newline-terminated. */
|
|
11
11
|
readonly stdout: string;
|
|
12
12
|
/** What goes to stderr: one line per warning, or nothing. */
|
|
@@ -29,3 +29,4 @@ export declare const FOREIGN_CONTENT_QUESTION = "avclient: overwrite them and ge
|
|
|
29
29
|
* are reported as a refused one's are.
|
|
30
30
|
*/
|
|
31
31
|
export declare function renderForeignContentCancellation(found: ForeignOutputContent, asked: boolean): string;
|
|
32
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type ProgressMode = "live" | "plain" | "off";
|
|
2
2
|
/** `live` only on a terminal that is not CI and accepts control sequences. */
|
|
3
3
|
export declare function progressModeOf(isTTY: boolean | undefined, env: Readonly<Record<string, string | undefined>>): Exclude<ProgressMode, "off">;
|
|
4
|
-
|
|
4
|
+
type StepReporterOptions = {
|
|
5
5
|
/** Where the lines go: the tool's stderr, so its stdout stays its result. */
|
|
6
6
|
readonly write: (text: string) => void;
|
|
7
7
|
readonly mode: ProgressMode;
|
|
@@ -29,3 +29,4 @@ export type StepReporter = {
|
|
|
29
29
|
/** `12.4s`, or `1m 02s` from a minute. */
|
|
30
30
|
export declare function formatElapsed(milliseconds: number): string;
|
|
31
31
|
export declare function createStepReporter(options: StepReporterOptions): StepReporter;
|
|
32
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type TerminalPrompter = {
|
|
2
2
|
readonly ask: (prompt: string) => Promise<string>;
|
|
3
3
|
readonly confirm: (question: string) => Promise<boolean>;
|
|
4
4
|
readonly close: () => void;
|
|
5
5
|
};
|
|
6
6
|
export declare function createTerminalPrompter(): TerminalPrompter;
|
|
7
|
+
export {};
|
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
/** A deferred read of one environment variable from the resolved cascade. */
|
|
2
|
-
export interface
|
|
2
|
+
export interface AvEnvReference {
|
|
3
3
|
/** Always `"env"`. */
|
|
4
4
|
readonly kind: "env";
|
|
5
5
|
/** The environment variable's name. */
|
|
6
6
|
readonly name: string;
|
|
7
7
|
}
|
|
8
8
|
/** A value written literally, or read from the resolved environment. */
|
|
9
|
-
export type
|
|
9
|
+
export type AvClientConfigValue = string | AvEnvReference;
|
|
10
10
|
/**
|
|
11
11
|
* What a `framework.client.ts` default-exports through `defineClientConfig`.
|
|
12
|
-
* Plain data: an environment variable appears as an `
|
|
12
|
+
* Plain data: an environment variable appears as an `AvEnvReference` (`env("NAME")`),
|
|
13
13
|
* a name to look up, so the file can be evaluated before the environment is loaded.
|
|
14
14
|
*/
|
|
15
|
-
export interface
|
|
15
|
+
export interface AvClientConfigInput {
|
|
16
16
|
/** The full deployed framework entrypoint: origin plus mount path. */
|
|
17
|
-
readonly entrypoint:
|
|
17
|
+
readonly entrypoint: AvClientConfigValue;
|
|
18
18
|
/**
|
|
19
19
|
* The directory the client is generated into, relative to the config file's
|
|
20
20
|
* directory. It is shared: the generator writes only `AvClient.ts` and
|
|
21
21
|
* `generated/` in it, and never touches anything else there.
|
|
22
22
|
*/
|
|
23
|
-
readonly generateAt:
|
|
23
|
+
readonly generateAt: AvClientConfigValue;
|
|
24
24
|
/**
|
|
25
25
|
* The `tsconfig.json` the generated client is written for, relative to the
|
|
26
|
-
* config file's directory
|
|
26
|
+
* config file's directory. Set it only for a layout the discovery gets wrong (a
|
|
27
27
|
* monorepo, a non-standard name). Absent, the nearest `tsconfig.json` above
|
|
28
28
|
* `generateAt` is used.
|
|
29
29
|
*/
|
|
30
30
|
readonly tsconfigFile?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The origins other than the entrypoint's that `$access` may send a
|
|
33
|
+
* sign-in to (`"https://auth.example.com"`): where the deployment mounted
|
|
34
|
+
* Aventara Access, when that is another origin. Absent, the generator
|
|
35
|
+
* refuses a ClientContract whose Access URL is on another origin.
|
|
36
|
+
*/
|
|
37
|
+
readonly accessOrigins?: readonly AvClientConfigValue[];
|
|
31
38
|
}
|
|
32
39
|
/** A filesystem path known to be absolute. */
|
|
33
40
|
export type AbsolutePath = string & {
|
|
@@ -44,11 +51,21 @@ export interface ClientEntrypoint {
|
|
|
44
51
|
readonly deployment: URL;
|
|
45
52
|
readonly path: EntrypointPath;
|
|
46
53
|
}
|
|
54
|
+
/** An entrypoint on the page's own origin: its mount path alone, such as `/api`. */
|
|
55
|
+
export interface SameOriginEntrypoint {
|
|
56
|
+
/** Absent: the deployment is whichever origin served the page. */
|
|
57
|
+
readonly deployment?: undefined;
|
|
58
|
+
readonly path: EntrypointPath;
|
|
59
|
+
}
|
|
60
|
+
/** The entrypoint a generated client embeds as its default: a deployment's, or the page's own origin. */
|
|
61
|
+
export type EmittedEntrypoint = ClientEntrypoint | SameOriginEntrypoint;
|
|
47
62
|
export interface ResolvedClientConfig {
|
|
48
63
|
readonly entrypoint: ClientEntrypoint;
|
|
49
64
|
/** `generateAt`, made absolute. */
|
|
50
65
|
readonly generateAt: AbsolutePath;
|
|
51
66
|
/** `tsconfigFile`, made absolute; absent when the config names none. */
|
|
52
67
|
readonly tsconfigFile?: AbsolutePath;
|
|
68
|
+
/** `accessOrigins`, each an http(s) origin (`scheme://host[:port]`); empty when the config names none. */
|
|
69
|
+
readonly accessOrigins: readonly string[];
|
|
53
70
|
readonly mode: string;
|
|
54
71
|
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AvClientConfigInput } from "./client-config.interface.js";
|
|
2
2
|
/** Prisma's `SUPPORTED_EXTENSIONS`, in its order. */
|
|
3
3
|
export declare const CLIENT_CONFIG_FILE_EXTENSIONS: readonly [".js", ".ts", ".mjs", ".cjs", ".mts", ".cts"];
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The same extensions as the docs and `avclient --help` list them, TypeScript
|
|
6
|
+
* first: the order a sentence to the developer names them in. Lookup keeps
|
|
7
|
+
* {@link CLIENT_CONFIG_FILE_EXTENSIONS}' order, Prisma's.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CLIENT_CONFIG_FILE_EXTENSIONS_AS_LISTED: readonly [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
|
|
6
10
|
/** The config file `avclient init` writes when the project has none. */
|
|
7
11
|
export declare const CLIENT_CONFIG_FILE = "framework.client.ts";
|
|
8
|
-
|
|
12
|
+
interface LoadedClientConfig {
|
|
9
13
|
/** The config file's absolute path; `generateAt` resolves against its directory. */
|
|
10
14
|
readonly file: string;
|
|
11
|
-
readonly config:
|
|
15
|
+
readonly config: AvClientConfigInput;
|
|
12
16
|
}
|
|
13
17
|
/**
|
|
14
18
|
* The config files present in `directory`, by name, in
|
|
@@ -20,13 +24,14 @@ export declare function clientConfigFilesIn(directory: string): readonly string[
|
|
|
20
24
|
* The one config file in `directory`, by name, or `undefined` when there is
|
|
21
25
|
* none.
|
|
22
26
|
*
|
|
23
|
-
* @throws
|
|
27
|
+
* @throws AvClientConfigError when there is more than one, naming each.
|
|
24
28
|
*/
|
|
25
29
|
export declare function findClientConfigFile(directory: string): string | undefined;
|
|
26
30
|
/**
|
|
27
31
|
* Evaluates the one `framework.client.<ext>` in `directory`.
|
|
28
32
|
*
|
|
29
|
-
* @throws
|
|
33
|
+
* @throws AvClientConfigError, in one line, when there is none or more than one,
|
|
30
34
|
* or the file does not evaluate, or does not default-export a client config.
|
|
31
35
|
*/
|
|
32
36
|
export declare function loadClientConfigFile(directory: string): Promise<LoadedClientConfig>;
|
|
37
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { AvClientConfigError } from "./config.resolver.js";
|
|
4
4
|
export const CLIENT_CONFIG_FILE_EXTENSIONS = [
|
|
5
5
|
".js",
|
|
6
6
|
".ts",
|
|
@@ -9,7 +9,15 @@ export const CLIENT_CONFIG_FILE_EXTENSIONS = [
|
|
|
9
9
|
".mts",
|
|
10
10
|
".cts",
|
|
11
11
|
];
|
|
12
|
-
export const
|
|
12
|
+
export const CLIENT_CONFIG_FILE_EXTENSIONS_AS_LISTED = [
|
|
13
|
+
".ts",
|
|
14
|
+
".mts",
|
|
15
|
+
".cts",
|
|
16
|
+
".js",
|
|
17
|
+
".mjs",
|
|
18
|
+
".cjs",
|
|
19
|
+
];
|
|
20
|
+
const CLIENT_CONFIG_BASENAME = "framework.client";
|
|
13
21
|
export const CLIENT_CONFIG_FILE = `${CLIENT_CONFIG_BASENAME}.ts`;
|
|
14
22
|
export function clientConfigFilesIn(directory) {
|
|
15
23
|
return CLIENT_CONFIG_FILE_EXTENSIONS.map((extension) => `${CLIENT_CONFIG_BASENAME}${extension}`).filter((name) => statSync(path.resolve(directory, name), {
|
|
@@ -20,14 +28,15 @@ export function findClientConfigFile(directory) {
|
|
|
20
28
|
const present = clientConfigFilesIn(directory);
|
|
21
29
|
if (present.length > 1) {
|
|
22
30
|
const named = `${present.slice(0, -1).join(", ")} and ${present.at(-1)}`;
|
|
23
|
-
throw new
|
|
31
|
+
throw new AvClientConfigError(`${named} are ${present.length === 2 ? "both" : "all"} in ${path.resolve(directory)}, and only one may configure the generator; keep one and delete the other${present.length > 2 ? "s" : ""}.`);
|
|
24
32
|
}
|
|
25
33
|
return present[0];
|
|
26
34
|
}
|
|
27
35
|
export async function loadClientConfigFile(directory) {
|
|
28
36
|
const name = findClientConfigFile(directory);
|
|
29
37
|
if (name === undefined) {
|
|
30
|
-
|
|
38
|
+
const [first, ...others] = CLIENT_CONFIG_FILE_EXTENSIONS_AS_LISTED;
|
|
39
|
+
throw new AvClientConfigError(`no ${CLIENT_CONFIG_BASENAME}${first}, ${others.slice(0, -1).join(", ")} or ${others.at(-1)} in ${path.resolve(directory)}; create ${CLIENT_CONFIG_FILE} with ` +
|
|
31
40
|
"`export default defineClientConfig({ entrypoint, generateAt })` (or run `avclient init`) and run the generator from its directory.");
|
|
32
41
|
}
|
|
33
42
|
const file = path.resolve(directory, name);
|
|
@@ -52,24 +61,33 @@ export async function loadClientConfigFile(directory) {
|
|
|
52
61
|
evaluated = loaded.layers?.find((layer) => layer.configFile === file)?.config;
|
|
53
62
|
}
|
|
54
63
|
catch (error) {
|
|
55
|
-
throw new
|
|
64
|
+
throw new AvClientConfigError(`${file} could not be evaluated: ${firstLineOf(error)}`, { cause: error });
|
|
56
65
|
}
|
|
57
66
|
return { file, config: clientConfigOf(file, evaluated) };
|
|
58
67
|
}
|
|
59
68
|
function clientConfigOf(file, config) {
|
|
60
69
|
const shape = "`export default defineClientConfig({ entrypoint, generateAt })`";
|
|
61
70
|
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
62
|
-
throw new
|
|
71
|
+
throw new AvClientConfigError(`${file}'s default export is not an object; it must be ${shape}.`);
|
|
63
72
|
}
|
|
64
73
|
const members = config;
|
|
65
74
|
const tsconfigFile = members.tsconfigFile;
|
|
66
75
|
if (tsconfigFile !== undefined && typeof tsconfigFile !== "string") {
|
|
67
|
-
throw new
|
|
76
|
+
throw new AvClientConfigError(`${file}'s tsconfigFile must be a string, a path relative to ${path.dirname(file)}.`);
|
|
77
|
+
}
|
|
78
|
+
const accessOrigins = members.accessOrigins;
|
|
79
|
+
if (accessOrigins !== undefined && !Array.isArray(accessOrigins)) {
|
|
80
|
+
throw new AvClientConfigError(`${file}'s accessOrigins must be a list of origins ("https://auth.example.com"), each a string or env("NAME").`);
|
|
68
81
|
}
|
|
69
82
|
return {
|
|
70
83
|
entrypoint: configValueOf(file, "entrypoint", members.entrypoint),
|
|
71
84
|
generateAt: configValueOf(file, "generateAt", members.generateAt),
|
|
72
85
|
...(tsconfigFile === undefined ? {} : { tsconfigFile }),
|
|
86
|
+
...(accessOrigins === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: {
|
|
89
|
+
accessOrigins: accessOrigins.map((origin) => configValueOf(file, "accessOrigins", origin)),
|
|
90
|
+
}),
|
|
73
91
|
};
|
|
74
92
|
}
|
|
75
93
|
function configValueOf(file, member, value) {
|
|
@@ -84,7 +102,7 @@ function configValueOf(file, member, value) {
|
|
|
84
102
|
return { kind: "env", name };
|
|
85
103
|
}
|
|
86
104
|
}
|
|
87
|
-
throw new
|
|
105
|
+
throw new AvClientConfigError(`${file}'s ${member} must be a string or env("NAME"), and it is ${value === undefined ? "missing" : "neither"}.`);
|
|
88
106
|
}
|
|
89
107
|
function firstLineOf(error) {
|
|
90
108
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AvClientConfigInput, AvEnvReference, ClientEntrypoint, EmittedEntrypoint, ResolvedClientConfig, SameOriginEntrypoint } from "./client-config.interface.js";
|
|
2
2
|
import type { EnvCascadeResolution } from "./env.cascade.js";
|
|
3
3
|
/** Gives a `framework.client.ts` its type: returns `config` unchanged. */
|
|
4
|
-
export declare function defineClientConfig(config:
|
|
4
|
+
export declare function defineClientConfig(config: AvClientConfigInput): AvClientConfigInput;
|
|
5
5
|
/** Names an environment variable to be read from the resolved environment. */
|
|
6
|
-
export declare function env(name: string):
|
|
6
|
+
export declare function env(name: string): AvEnvReference;
|
|
7
7
|
/**
|
|
8
8
|
* The configuration cannot be resolved. The message is the whole diagnosis — the
|
|
9
9
|
* CLI prints it and exits non-zero; no stack is owed for a user's mistake.
|
|
10
10
|
*/
|
|
11
|
-
export declare class
|
|
12
|
-
readonly name = "
|
|
11
|
+
export declare class AvClientConfigError extends Error {
|
|
12
|
+
readonly name = "AvClientConfigError";
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
readonly config:
|
|
14
|
+
interface ClientConfigResolutionInput {
|
|
15
|
+
readonly config: AvClientConfigInput;
|
|
16
16
|
readonly cascade: EnvCascadeResolution;
|
|
17
17
|
/** The directory holding the config file; `generateAt` resolves against it. */
|
|
18
18
|
readonly configDirectory: string;
|
|
@@ -23,7 +23,14 @@ export interface ClientConfigResolutionInput {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function resolveClientConfig(input: ClientConfigResolutionInput): ResolvedClientConfig;
|
|
25
25
|
/**
|
|
26
|
-
* @throws
|
|
26
|
+
* @throws AvClientConfigError in one sentence that does not echo the value.
|
|
27
27
|
*/
|
|
28
28
|
export declare function resolveEntrypoint(value: string): ClientEntrypoint;
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The same-origin entrypoint of a framework mounted at `api`.
|
|
31
|
+
*
|
|
32
|
+
* @throws AvClientConfigError when `api` is not a mount path.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sameOriginEntrypoint(api: string): SameOriginEntrypoint;
|
|
35
|
+
export declare function entrypointHref(entrypoint: EmittedEntrypoint): string;
|
|
36
|
+
export {};
|
|
@@ -6,15 +6,15 @@ export function defineClientConfig(config) {
|
|
|
6
6
|
export function env(name) {
|
|
7
7
|
return { kind: "env", name };
|
|
8
8
|
}
|
|
9
|
-
export class
|
|
10
|
-
name = "
|
|
9
|
+
export class AvClientConfigError extends Error {
|
|
10
|
+
name = "AvClientConfigError";
|
|
11
11
|
}
|
|
12
12
|
export function resolveClientConfig(input) {
|
|
13
13
|
const entrypoint = resolveValue("entrypoint", input.config.entrypoint, input.cascade);
|
|
14
14
|
const generateAt = resolveValue("generateAt", input.config.generateAt, input.cascade);
|
|
15
15
|
const tsconfigFile = input.config.tsconfigFile;
|
|
16
16
|
if (tsconfigFile === "") {
|
|
17
|
-
throw new
|
|
17
|
+
throw new AvClientConfigError("tsconfigFile is empty.");
|
|
18
18
|
}
|
|
19
19
|
return {
|
|
20
20
|
entrypoint: resolveEntrypoint(entrypoint),
|
|
@@ -24,18 +24,36 @@ export function resolveClientConfig(input) {
|
|
|
24
24
|
: {
|
|
25
25
|
tsconfigFile: path.resolve(input.configDirectory, tsconfigFile),
|
|
26
26
|
}),
|
|
27
|
+
accessOrigins: (input.config.accessOrigins ?? []).map((origin) => resolveAccessOrigin(resolveValue("accessOrigins", origin, input.cascade))),
|
|
27
28
|
mode: input.cascade.mode,
|
|
28
29
|
};
|
|
29
30
|
}
|
|
31
|
+
function resolveAccessOrigin(value) {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(value);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
url = undefined;
|
|
38
|
+
}
|
|
39
|
+
if (url === undefined ||
|
|
40
|
+
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
41
|
+
url.username !== "" ||
|
|
42
|
+
url.password !== "" ||
|
|
43
|
+
url.origin !== value.replace(/\/$/u, "").toLowerCase()) {
|
|
44
|
+
throw new AvClientConfigError('accessOrigins holds a value that is no http(s) origin; each must be scheme, host and port alone ("https://auth.example.com").');
|
|
45
|
+
}
|
|
46
|
+
return url.origin;
|
|
47
|
+
}
|
|
30
48
|
function resolveValue(field, value, cascade) {
|
|
31
49
|
if (typeof value === "string") {
|
|
32
50
|
if (value === "")
|
|
33
|
-
throw new
|
|
51
|
+
throw new AvClientConfigError(`${field} is empty.`);
|
|
34
52
|
return value;
|
|
35
53
|
}
|
|
36
54
|
const resolved = cascade.env[value.name];
|
|
37
55
|
if (resolved === undefined || resolved === "") {
|
|
38
|
-
throw new
|
|
56
|
+
throw new AvClientConfigError(`${field} reads ${value.name}, which is not set in the process environment ` +
|
|
39
57
|
`or in any of ${cascade.candidates.join(", ")} (mode "${cascade.mode}").`);
|
|
40
58
|
}
|
|
41
59
|
return resolved;
|
|
@@ -43,32 +61,32 @@ function resolveValue(field, value, cascade) {
|
|
|
43
61
|
const ENTRYPOINT_IS = "it must be the origin plus a mount path";
|
|
44
62
|
export function resolveEntrypoint(value) {
|
|
45
63
|
if (/\s/u.test(value)) {
|
|
46
|
-
throw new
|
|
64
|
+
throw new AvClientConfigError(`entrypoint contains whitespace; ${ENTRYPOINT_IS}, written without spaces, tabs or line breaks.`);
|
|
47
65
|
}
|
|
48
66
|
let url;
|
|
49
67
|
try {
|
|
50
68
|
url = new URL(value);
|
|
51
69
|
}
|
|
52
70
|
catch {
|
|
53
|
-
throw new
|
|
71
|
+
throw new AvClientConfigError("entrypoint is not an absolute URL; it must be the deployed origin plus mount path.");
|
|
54
72
|
}
|
|
55
73
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
56
|
-
throw new
|
|
74
|
+
throw new AvClientConfigError(`entrypoint must use http: or https:, not ${url.protocol}`);
|
|
57
75
|
}
|
|
58
76
|
if (url.username !== "" || url.password !== "") {
|
|
59
|
-
throw new
|
|
77
|
+
throw new AvClientConfigError("entrypoint must not carry credentials: the platform's fetch refuses a URL that " +
|
|
60
78
|
"includes them, and the entrypoint is embedded in the generated client, which ships — " +
|
|
61
79
|
"authenticate through a custom fetch instead.");
|
|
62
80
|
}
|
|
63
81
|
if (value.includes("?") || value.includes("#")) {
|
|
64
|
-
throw new
|
|
82
|
+
throw new AvClientConfigError("entrypoint must not carry a query or fragment; it is the origin plus mount path only.");
|
|
65
83
|
}
|
|
66
84
|
if (value.split(/[/\\]/u).some(isDotDotSegment)) {
|
|
67
|
-
throw new
|
|
85
|
+
throw new AvClientConfigError(`entrypoint's mount path has a .. segment; ${ENTRYPOINT_IS}, with no dot segments.`);
|
|
68
86
|
}
|
|
69
87
|
const normalized = AvProtocol.normalizeEntrypoint(writtenMountPath(value));
|
|
70
88
|
if (!normalized.ok) {
|
|
71
|
-
throw new
|
|
89
|
+
throw new AvClientConfigError(`entrypoint's mount path ${normalized.message}.`);
|
|
72
90
|
}
|
|
73
91
|
const deployment = new URL(url.href);
|
|
74
92
|
deployment.pathname = "/";
|
|
@@ -77,7 +95,17 @@ export function resolveEntrypoint(value) {
|
|
|
77
95
|
function writtenMountPath(value) {
|
|
78
96
|
return value.replace(/^[A-Za-z][A-Za-z0-9+.-]*:[/\\]*[^/\\?#]*/u, "");
|
|
79
97
|
}
|
|
98
|
+
export function sameOriginEntrypoint(api) {
|
|
99
|
+
const normalized = AvProtocol.normalizeEntrypoint(api);
|
|
100
|
+
if (!normalized.ok) {
|
|
101
|
+
throw new AvClientConfigError(`api ${normalized.message}.`);
|
|
102
|
+
}
|
|
103
|
+
return { path: normalized.path };
|
|
104
|
+
}
|
|
80
105
|
export function entrypointHref(entrypoint) {
|
|
106
|
+
if (entrypoint.deployment === undefined) {
|
|
107
|
+
return entrypoint.path;
|
|
108
|
+
}
|
|
81
109
|
const url = new URL(entrypoint.deployment.href);
|
|
82
110
|
url.pathname = entrypoint.path === "" ? "/" : entrypoint.path;
|
|
83
111
|
return url.href.replace(/\/$/, "");
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
export type EnvRecord = Readonly<Record<string, string>>;
|
|
3
3
|
/** The process environment as Node types it: values may be `undefined`. */
|
|
4
4
|
export type ProcessEnvInput = Readonly<Record<string, string | undefined>>;
|
|
5
|
-
/** The mode used when neither an explicit mode nor `NODE_ENV` supplies one. */
|
|
6
|
-
export declare const DEFAULT_ENV_MODE = "development";
|
|
7
5
|
export type EnvFileReader = (filePath: string) => string | undefined;
|
|
8
|
-
|
|
6
|
+
interface EnvCascadeInput {
|
|
9
7
|
/** The directory the five candidates are looked up in. */
|
|
10
8
|
readonly directory: string;
|
|
11
9
|
/** A snapshot of the process environment. Never mutated. */
|
|
@@ -23,18 +21,15 @@ export interface EnvCascadeResolution {
|
|
|
23
21
|
readonly loaded: readonly string[];
|
|
24
22
|
}
|
|
25
23
|
/** A candidate exists but could not be read, or contains a discarded line. */
|
|
26
|
-
export declare class
|
|
24
|
+
export declare class AvEnvFileError extends Error {
|
|
27
25
|
readonly filePath: string;
|
|
28
26
|
readonly reason: "unreadable" | "malformed";
|
|
29
|
-
readonly name = "
|
|
27
|
+
readonly name = "AvEnvFileError";
|
|
30
28
|
constructor(filePath: string, reason: "unreadable" | "malformed", detail: string);
|
|
31
29
|
}
|
|
32
30
|
/** The four file candidates for a mode, highest precedence first. */
|
|
33
31
|
export declare function envCascadeCandidates(mode: string): readonly string[];
|
|
34
|
-
export declare function resolveEnvMode(processEnv: ProcessEnvInput, explicit?: string): string;
|
|
35
32
|
export declare function resolveEnvCascade(input: EnvCascadeInput): EnvCascadeResolution;
|
|
36
|
-
/** Reads a candidate from disk; `ENOENT` alone means "not there". */
|
|
37
|
-
export declare function readEnvFileFromDisk(filePath: string): string | undefined;
|
|
38
33
|
/**
|
|
39
34
|
* Parses one file's content. A file whose content holds a line the parser would
|
|
40
35
|
* silently discard is malformed: the user wrote something they expect to take
|
|
@@ -50,3 +45,4 @@ export declare function parseEnvFile(filePath: string, content: string): Record<
|
|
|
50
45
|
* spanning lines — or, when there is none, to the end of its own line.
|
|
51
46
|
*/
|
|
52
47
|
export declare function firstDiscardedLine(content: string): number | undefined;
|
|
48
|
+
export {};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseEnv } from "node:util";
|
|
4
|
-
|
|
5
|
-
export class
|
|
4
|
+
const DEFAULT_ENV_MODE = "development";
|
|
5
|
+
export class AvEnvFileError extends Error {
|
|
6
6
|
filePath;
|
|
7
7
|
reason;
|
|
8
|
-
name = "
|
|
8
|
+
name = "AvEnvFileError";
|
|
9
9
|
constructor(filePath, reason, detail) {
|
|
10
10
|
super(`${filePath}: ${detail}`);
|
|
11
11
|
this.filePath = filePath;
|
|
@@ -15,7 +15,7 @@ export class EnvFileError extends Error {
|
|
|
15
15
|
export function envCascadeCandidates(mode) {
|
|
16
16
|
return [`.env.${mode}.local`, `.env.${mode}`, ".env.local", ".env"];
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
function resolveEnvMode(processEnv, explicit) {
|
|
19
19
|
if (explicit !== undefined && explicit !== "")
|
|
20
20
|
return explicit;
|
|
21
21
|
const nodeEnv = processEnv.NODE_ENV;
|
|
@@ -44,7 +44,7 @@ export function resolveEnvCascade(input) {
|
|
|
44
44
|
}
|
|
45
45
|
return { mode, env: Object.freeze(env), candidates, loaded };
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
function readEnvFileFromDisk(filePath) {
|
|
48
48
|
try {
|
|
49
49
|
return readFileSync(filePath, "utf8");
|
|
50
50
|
}
|
|
@@ -52,14 +52,14 @@ export function readEnvFileFromDisk(filePath) {
|
|
|
52
52
|
if (isErrnoException(error) && error.code === "ENOENT")
|
|
53
53
|
return undefined;
|
|
54
54
|
const code = isErrnoException(error) ? error.code : undefined;
|
|
55
|
-
throw new
|
|
55
|
+
throw new AvEnvFileError(filePath, "unreadable", `exists but cannot be read (${code ?? "unknown error"})`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
export function parseEnvFile(filePath, content) {
|
|
59
59
|
const source = content.startsWith("") ? content.slice(1) : content;
|
|
60
60
|
const discarded = firstDiscardedLine(source);
|
|
61
61
|
if (discarded !== undefined) {
|
|
62
|
-
throw new
|
|
62
|
+
throw new AvEnvFileError(filePath, "malformed", `line ${discarded} is not a NAME=value assignment and would be ignored`);
|
|
63
63
|
}
|
|
64
64
|
return parseEnv(source);
|
|
65
65
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { ClientModuleStyle, ImportFileExtension, ModuleFormat } from "../emit/module-style.interface.js";
|
|
3
|
-
import { type LocatedTsconfig } from "./tsconfig.locator.js";
|
|
1
|
+
import type { ClientModuleStyle } from "../emit/module-style.interface.js";
|
|
4
2
|
/** What the run follows, and where it read it from. */
|
|
5
3
|
export interface ResolvedModuleStyle extends ClientModuleStyle {
|
|
6
4
|
/** The tsconfig the style was inferred from. */
|
|
7
5
|
readonly tsconfig: string;
|
|
8
6
|
}
|
|
9
|
-
|
|
7
|
+
interface ModuleStyleInput {
|
|
10
8
|
/** The absolute `generateAt`. */
|
|
11
9
|
readonly generateAt: string;
|
|
12
10
|
/** The generated entry file, absolute: the file the project must include. */
|
|
@@ -17,11 +15,10 @@ export interface ModuleStyleInput {
|
|
|
17
15
|
/**
|
|
18
16
|
* The style the project's tsconfig asks for.
|
|
19
17
|
*
|
|
20
|
-
* @throws
|
|
18
|
+
* @throws AvClientConfigError when there is no tsconfig to follow, or the named
|
|
21
19
|
* `tsconfigFile` cannot be read.
|
|
22
20
|
*/
|
|
23
21
|
export declare function resolveModuleStyle(input: ModuleStyleInput): ResolvedModuleStyle;
|
|
24
22
|
/** The refusal of a project with no tsconfig: a JavaScript project. */
|
|
25
23
|
export declare function noTsconfigRefusal(generateAt: string): string;
|
|
26
|
-
export
|
|
27
|
-
export declare function moduleFormatOf(tsconfig: LocatedTsconfig, generateAt: string): ModuleFormat;
|
|
24
|
+
export {};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { AvClientConfigError } from "./config.resolver.js";
|
|
4
4
|
import { locateTsconfig } from "./tsconfig.locator.js";
|
|
5
5
|
export function resolveModuleStyle(input) {
|
|
6
6
|
const located = locateTsconfig(input);
|
|
7
7
|
if (located === undefined) {
|
|
8
|
-
throw new
|
|
8
|
+
throw new AvClientConfigError(noTsconfigRefusal(input.generateAt));
|
|
9
9
|
}
|
|
10
10
|
return {
|
|
11
11
|
tsconfig: located.path,
|
|
@@ -20,7 +20,7 @@ export function noTsconfigRefusal(generateAt) {
|
|
|
20
20
|
"(`npx tsc --init` writes one), or, if it has one this search does not find, name it with `tsconfigFile` " +
|
|
21
21
|
"in your framework.client config. Nothing was written.");
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
function importFileExtensionOf(config) {
|
|
24
24
|
const options = config.compilerOptions;
|
|
25
25
|
if (options?.allowImportingTsExtensions ||
|
|
26
26
|
options?.rewriteRelativeImportExtensions) {
|
|
@@ -32,7 +32,7 @@ export function importFileExtensionOf(config) {
|
|
|
32
32
|
? ""
|
|
33
33
|
: "js";
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
function moduleFormatOf(tsconfig, generateAt) {
|
|
36
36
|
const module = tsconfig.config.compilerOptions?.module?.toLowerCase();
|
|
37
37
|
if (module === undefined || module === "") {
|
|
38
38
|
return "esm";
|
|
@@ -4,7 +4,7 @@ export interface LocatedTsconfig {
|
|
|
4
4
|
readonly path: string;
|
|
5
5
|
readonly config: TsConfigJsonResolved;
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
interface TsconfigLocation {
|
|
8
8
|
/** The absolute `generateAt`: the search starts there. */
|
|
9
9
|
readonly generateAt: string;
|
|
10
10
|
/** The generated entry file the project must include, absolute. */
|
|
@@ -16,6 +16,7 @@ export interface TsconfigLocation {
|
|
|
16
16
|
* The tsconfig `location` names, or `undefined` when no `tsconfig.json` is in
|
|
17
17
|
* `generateAt` or any directory above it (and none was named).
|
|
18
18
|
*
|
|
19
|
-
* @throws
|
|
19
|
+
* @throws AvClientConfigError when the named `tsconfigFile` is missing or unreadable.
|
|
20
20
|
*/
|
|
21
21
|
export declare function locateTsconfig(location: TsconfigLocation): LocatedTsconfig | undefined;
|
|
22
|
+
export {};
|