@aventara/client 0.1.0-pilot.1 → 0.1.0-pilot.2
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/README.md +41 -7
- package/dist/avclient.bin.js +0 -10
- package/dist/cli/command.parser.d.ts +15 -10
- package/dist/cli/command.parser.js +13 -19
- package/dist/cli/generate.command.js +0 -6
- package/dist/cli/generation-failure.renderer.js +0 -14
- package/dist/cli/generation-success.renderer.d.ts +4 -1
- package/dist/cli/generation-success.renderer.js +0 -13
- package/dist/cli/terminal.prompter.d.ts +1 -2
- package/dist/cli/warning.renderer.d.ts +2 -2
- package/dist/cli/warning.renderer.js +0 -8
- package/dist/cli.d.ts +8 -16
- package/dist/cli.js +5 -25
- package/dist/config/client-config.interface.d.ts +18 -16
- package/dist/config/client-config.interface.js +0 -13
- package/dist/config/config.loader.d.ts +34 -22
- package/dist/config/config.loader.js +49 -52
- package/dist/config/config.resolver.d.ts +13 -19
- package/dist/config/config.resolver.js +9 -48
- package/dist/config/env.cascade.d.ts +12 -14
- package/dist/config/env.cascade.js +0 -19
- package/dist/config/module-style.resolver.d.ts +52 -0
- package/dist/config/module-style.resolver.js +75 -0
- package/dist/config/tsconfig.locator.d.ts +45 -0
- package/dist/config/tsconfig.locator.js +52 -0
- package/dist/contract/contract.acceptance.d.ts +12 -26
- package/dist/contract/contract.acceptance.js +0 -54
- package/dist/contract/contract.fetcher.d.ts +12 -17
- package/dist/contract/contract.fetcher.js +0 -24
- package/dist/contract/contract.loader.d.ts +4 -5
- package/dist/contract/contract.loader.js +0 -10
- package/dist/emit/banner.emitter.d.ts +11 -12
- package/dist/emit/banner.emitter.js +0 -26
- package/dist/emit/client-surface.emitter.d.ts +17 -21
- package/dist/emit/client-surface.emitter.js +29 -55
- package/dist/emit/client-tree.emitter.d.ts +11 -20
- package/dist/emit/client-tree.emitter.js +12 -54
- package/dist/emit/contract-carrier.emitter.d.ts +5 -6
- package/dist/emit/contract-carrier.emitter.js +0 -28
- package/dist/emit/derivation.emitter.d.ts +7 -7
- package/dist/emit/derivation.emitter.js +2 -161
- package/dist/emit/descriptor.emitter.js +2 -28
- package/dist/emit/emitted-tree.interface.d.ts +40 -17
- package/dist/emit/emitted-tree.interface.js +6 -16
- package/dist/emit/enum.emitter.d.ts +4 -4
- package/dist/emit/enum.emitter.js +0 -24
- package/dist/emit/module-specifier.scanner.d.ts +25 -0
- package/dist/emit/module-specifier.scanner.js +160 -0
- package/dist/emit/module-style.interface.d.ts +58 -0
- package/dist/emit/module-style.interface.js +8 -0
- package/dist/emit/name.deriver.d.ts +33 -61
- package/dist/emit/name.deriver.js +0 -134
- package/dist/emit/named-type.emitter.d.ts +14 -21
- package/dist/emit/named-type.emitter.js +3 -30
- package/dist/emit/runtime.emitter.d.ts +23 -50
- package/dist/emit/runtime.emitter.js +68 -159
- package/dist/emit/scalar.codec.d.ts +20 -33
- package/dist/emit/scalar.codec.js +13 -69
- package/dist/emit/transaction.emitter.d.ts +6 -14
- package/dist/emit/transaction.emitter.js +24 -33
- package/dist/generate.d.ts +17 -34
- package/dist/generate.js +14 -22
- package/dist/index.js +0 -5
- package/dist/init/client-config.template.d.ts +6 -4
- package/dist/init/client-config.template.js +10 -13
- package/dist/init/client-init.errors.js +0 -3
- package/dist/init/client-init.orchestrator.js +1 -9
- package/dist/init/client-init.planner.d.ts +1 -9
- package/dist/init/client-init.planner.js +6 -24
- package/dist/init/client-init.questions.d.ts +8 -12
- package/dist/init/client-init.questions.js +0 -11
- package/dist/init/client-project.inspector.d.ts +6 -0
- package/dist/init/client-project.inspector.js +2 -2
- package/dist/node-version.guard.js +0 -12
- package/dist/output/output.validator.d.ts +22 -22
- package/dist/output/output.validator.js +46 -59
- package/dist/output/output.writer.d.ts +56 -52
- package/dist/output/output.writer.js +71 -133
- package/package.json +6 -4
|
@@ -1,77 +1,75 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
3
|
import { ClientConfigError } from "./config.resolver.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* import of theirs would.
|
|
23
|
-
*
|
|
24
|
-
* The evaluated value is checked against `ClientConfigInput`'s shape before it
|
|
25
|
-
* is trusted: the config is the consumer's code, and a typo there should be a
|
|
26
|
-
* sentence naming the member, not a `TypeError` from deep inside resolution.
|
|
27
|
-
*/
|
|
28
|
-
/** The config file `avclient init` writes: an ES module whatever the project's `type` (pilot.1). */
|
|
29
|
-
export const CLIENT_CONFIG_FILE = "framework.client.mts";
|
|
30
|
-
/** §15.2's spelling, still read for the setups written before pilot.1. */
|
|
31
|
-
export const LEGACY_CLIENT_CONFIG_FILE = "framework.client.ts";
|
|
32
|
-
/**
|
|
33
|
-
* Evaluates `<directory>/framework.client.mts`, or `framework.client.ts` when
|
|
34
|
-
* only that one is there.
|
|
35
|
-
*
|
|
36
|
-
* @throws ClientConfigError, in one line, when neither or both are there, or
|
|
37
|
-
* the file does not evaluate, or does not default-export a client config.
|
|
38
|
-
*/
|
|
39
|
-
export async function loadClientConfigFile(directory) {
|
|
40
|
-
const present = [CLIENT_CONFIG_FILE, LEGACY_CLIENT_CONFIG_FILE].filter((name) => existsSync(path.resolve(directory, name)));
|
|
4
|
+
export const CLIENT_CONFIG_FILE_EXTENSIONS = [
|
|
5
|
+
".js",
|
|
6
|
+
".ts",
|
|
7
|
+
".mjs",
|
|
8
|
+
".cjs",
|
|
9
|
+
".mts",
|
|
10
|
+
".cts",
|
|
11
|
+
];
|
|
12
|
+
export const CLIENT_CONFIG_BASENAME = "framework.client";
|
|
13
|
+
export const CLIENT_CONFIG_FILE = `${CLIENT_CONFIG_BASENAME}.ts`;
|
|
14
|
+
export function clientConfigFilesIn(directory) {
|
|
15
|
+
return CLIENT_CONFIG_FILE_EXTENSIONS.map((extension) => `${CLIENT_CONFIG_BASENAME}${extension}`).filter((name) => statSync(path.resolve(directory, name), {
|
|
16
|
+
throwIfNoEntry: false,
|
|
17
|
+
})?.isFile() === true);
|
|
18
|
+
}
|
|
19
|
+
export function findClientConfigFile(directory) {
|
|
20
|
+
const present = clientConfigFilesIn(directory);
|
|
41
21
|
if (present.length > 1) {
|
|
42
|
-
|
|
22
|
+
const named = `${present.slice(0, -1).join(", ")} and ${present.at(-1)}`;
|
|
23
|
+
throw new ClientConfigError(`${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" : ""}.`);
|
|
43
24
|
}
|
|
44
|
-
|
|
25
|
+
return present[0];
|
|
26
|
+
}
|
|
27
|
+
export async function loadClientConfigFile(directory) {
|
|
28
|
+
const name = findClientConfigFile(directory);
|
|
45
29
|
if (name === undefined) {
|
|
46
|
-
throw new ClientConfigError(`no ${
|
|
30
|
+
throw new ClientConfigError(`no ${CLIENT_CONFIG_BASENAME}.{${CLIENT_CONFIG_FILE_EXTENSIONS.map((extension) => extension.slice(1)).join(",")}} in ${path.resolve(directory)}; create ${CLIENT_CONFIG_FILE} with ` +
|
|
47
31
|
"`export default defineClientConfig({ entrypoint, generateAt })` (or run `avclient init`) and run the generator from its directory.");
|
|
48
32
|
}
|
|
49
33
|
const file = path.resolve(directory, name);
|
|
50
34
|
let evaluated;
|
|
51
35
|
try {
|
|
52
|
-
|
|
36
|
+
const { loadConfig } = await import("c12");
|
|
37
|
+
const loaded = await loadConfig({
|
|
38
|
+
cwd: path.dirname(file),
|
|
39
|
+
name: CLIENT_CONFIG_BASENAME,
|
|
40
|
+
configFile: file,
|
|
41
|
+
dotenv: false,
|
|
42
|
+
rcFile: false,
|
|
43
|
+
giget: false,
|
|
44
|
+
extend: false,
|
|
45
|
+
packageJson: false,
|
|
46
|
+
jitiOptions: {
|
|
47
|
+
interopDefault: true,
|
|
48
|
+
moduleCache: false,
|
|
49
|
+
extensions: [...CLIENT_CONFIG_FILE_EXTENSIONS],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
evaluated = loaded.layers?.find((layer) => layer.configFile === file)?.config;
|
|
53
53
|
}
|
|
54
54
|
catch (error) {
|
|
55
|
-
throw new ClientConfigError(`${file} could not be evaluated: ${firstLineOf(error)}
|
|
56
|
-
? `; if this project's package.json says "type": "commonjs" (npm init -y writes it), Node reads a .ts file as CommonJS — rename it ${CLIENT_CONFIG_FILE}, which is an ES module in every project`
|
|
57
|
-
: ""}`, { cause: error });
|
|
55
|
+
throw new ClientConfigError(`${file} could not be evaluated: ${firstLineOf(error)}`, { cause: error });
|
|
58
56
|
}
|
|
59
57
|
return { file, config: clientConfigOf(file, evaluated) };
|
|
60
58
|
}
|
|
61
|
-
|
|
62
|
-
function clientConfigOf(file, evaluated) {
|
|
59
|
+
function clientConfigOf(file, config) {
|
|
63
60
|
const shape = "`export default defineClientConfig({ entrypoint, generateAt })`";
|
|
64
|
-
if (!Object.hasOwn(evaluated, "default")) {
|
|
65
|
-
throw new ClientConfigError(`${file} has no default export; it must be ${shape}.`);
|
|
66
|
-
}
|
|
67
|
-
const config = evaluated.default;
|
|
68
61
|
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
69
62
|
throw new ClientConfigError(`${file}'s default export is not an object; it must be ${shape}.`);
|
|
70
63
|
}
|
|
71
64
|
const members = config;
|
|
65
|
+
const tsconfigFile = members.tsconfigFile;
|
|
66
|
+
if (tsconfigFile !== undefined && typeof tsconfigFile !== "string") {
|
|
67
|
+
throw new ClientConfigError(`${file}'s tsconfigFile must be a string, a path relative to ${path.dirname(file)}.`);
|
|
68
|
+
}
|
|
72
69
|
return {
|
|
73
70
|
entrypoint: configValueOf(file, "entrypoint", members.entrypoint),
|
|
74
71
|
generateAt: configValueOf(file, "generateAt", members.generateAt),
|
|
72
|
+
...(tsconfigFile === undefined ? {} : { tsconfigFile }),
|
|
75
73
|
};
|
|
76
74
|
}
|
|
77
75
|
function configValueOf(file, member, value) {
|
|
@@ -88,7 +86,6 @@ function configValueOf(file, member, value) {
|
|
|
88
86
|
}
|
|
89
87
|
throw new ClientConfigError(`${file}'s ${member} must be a string or env("NAME"), and it is ${value === undefined ? "missing" : "neither"}.`);
|
|
90
88
|
}
|
|
91
|
-
/** An error's message, cut at its first line break: the refusal is one line. */
|
|
92
89
|
function firstLineOf(error) {
|
|
93
90
|
const message = error instanceof Error ? error.message : String(error);
|
|
94
91
|
return message.split("\n", 1)[0] ?? "";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ClientConfigInput, ClientEntrypoint, EnvReference, ResolvedClientConfig } from "./client-config.interface.js";
|
|
2
2
|
import type { EnvCascadeResolution } from "./env.cascade.js";
|
|
3
|
-
/** The identity function that gives a `framework.client.ts` its type
|
|
3
|
+
/** The identity function that gives a `framework.client.ts` its type. */
|
|
4
4
|
export declare function defineClientConfig(config: ClientConfigInput): ClientConfigInput;
|
|
5
|
-
/** Names an environment variable to be read from the resolved cascade
|
|
5
|
+
/** Names an environment variable to be read from the resolved cascade. */
|
|
6
6
|
export declare function env(name: string): EnvReference;
|
|
7
7
|
/**
|
|
8
8
|
* The configuration cannot be resolved. The message is the whole diagnosis — the
|
|
@@ -23,28 +23,22 @@ export interface ClientConfigResolutionInput {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function resolveClientConfig(input: ClientConfigResolutionInput): ResolvedClientConfig;
|
|
25
25
|
/**
|
|
26
|
-
* An entrypoint value resolved to its canonical form
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* value as written.
|
|
26
|
+
* An entrypoint value resolved to its canonical form. The URL half is this
|
|
27
|
+
* function's: whitespace (which the URL parser would trim or drop unseen), a value
|
|
28
|
+
* that is not an absolute URL, a scheme other than http(s), credentials, a query
|
|
29
|
+
* or fragment (even an empty one, which the parser forgets), and a `..` segment
|
|
30
|
+
* (which the parser would resolve away) are refused here, reading the value as
|
|
31
|
+
* written.
|
|
33
32
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* character — goes to `AvProtocol.normalizeEntrypoint`, the rule the server's
|
|
37
|
-
* config resolution applies, so the two halves of F-822 cannot disagree
|
|
38
|
-
* (`entrypoint.parity.spec.ts`). Only its slash spelling is coerced — `api`,
|
|
39
|
-
* `/api/` and `//api//` are one intent, `/api` — and root is `""`.
|
|
33
|
+
* Only its slash spelling is coerced — `api`, `/api/` and `//api//` are one
|
|
34
|
+
* intent, `/api` — and root is `""`.
|
|
40
35
|
*
|
|
41
36
|
* @throws ClientConfigError in one sentence that does not echo the value.
|
|
42
37
|
*/
|
|
43
38
|
export declare function resolveEntrypoint(value: string): ClientEntrypoint;
|
|
44
39
|
/**
|
|
45
|
-
* The entrypoint as one absolute URL with no trailing slash — the form the
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* Root is the bare origin.
|
|
40
|
+
* The entrypoint as one absolute URL with no trailing slash — the form the emitted
|
|
41
|
+
* transport joins its routes to, and the default a generated client embeds. Root
|
|
42
|
+
* is the bare origin.
|
|
49
43
|
*/
|
|
50
44
|
export declare function entrypointHref(entrypoint: ClientEntrypoint): string;
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { AvProtocol } from "@aventara/core/protocol";
|
|
3
|
-
/** The identity function that gives a `framework.client.ts` its type (§15.2). */
|
|
4
3
|
export function defineClientConfig(config) {
|
|
5
4
|
return config;
|
|
6
5
|
}
|
|
7
|
-
/** Names an environment variable to be read from the resolved cascade (§15.2). */
|
|
8
6
|
export function env(name) {
|
|
9
7
|
return { kind: "env", name };
|
|
10
8
|
}
|
|
11
|
-
/**
|
|
12
|
-
* The configuration cannot be resolved. The message is the whole diagnosis — the
|
|
13
|
-
* CLI prints it and exits non-zero; no stack is owed for a user's mistake.
|
|
14
|
-
*/
|
|
15
9
|
export class ClientConfigError extends Error {
|
|
16
10
|
name = "ClientConfigError";
|
|
17
11
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Resolves a config against an already-resolved cascade. Pure: it reads no
|
|
20
|
-
* file and no global, so it is tested against literal inputs.
|
|
21
|
-
*/
|
|
22
12
|
export function resolveClientConfig(input) {
|
|
23
13
|
const entrypoint = resolveValue("entrypoint", input.config.entrypoint, input.cascade);
|
|
24
14
|
const generateAt = resolveValue("generateAt", input.config.generateAt, input.cascade);
|
|
15
|
+
const tsconfigFile = input.config.tsconfigFile;
|
|
16
|
+
if (tsconfigFile === "") {
|
|
17
|
+
throw new ClientConfigError("tsconfigFile is empty.");
|
|
18
|
+
}
|
|
25
19
|
return {
|
|
26
20
|
entrypoint: resolveEntrypoint(entrypoint),
|
|
27
21
|
generateAt: path.resolve(input.configDirectory, generateAt),
|
|
22
|
+
...(tsconfigFile === undefined
|
|
23
|
+
? {}
|
|
24
|
+
: {
|
|
25
|
+
tsconfigFile: path.resolve(input.configDirectory, tsconfigFile),
|
|
26
|
+
}),
|
|
28
27
|
mode: input.cascade.mode,
|
|
29
28
|
};
|
|
30
29
|
}
|
|
@@ -41,29 +40,7 @@ function resolveValue(field, value, cascade) {
|
|
|
41
40
|
}
|
|
42
41
|
return resolved;
|
|
43
42
|
}
|
|
44
|
-
/**
|
|
45
|
-
* The refusal's closing clause: what an entrypoint is. Never the value itself —
|
|
46
|
-
* an entrypoint may carry credentials.
|
|
47
|
-
*/
|
|
48
43
|
const ENTRYPOINT_IS = "it must be the origin plus a mount path";
|
|
49
|
-
/**
|
|
50
|
-
* An entrypoint value resolved to its canonical form (F-822, architect's
|
|
51
|
-
* decision 2026-10-04; Q15). The URL half is this function's: whitespace (which
|
|
52
|
-
* the URL parser would trim or drop unseen), a value that is not an absolute
|
|
53
|
-
* URL, a scheme other than http(s), credentials (Q5, architect 2026-10-05), a
|
|
54
|
-
* query or fragment (even an empty one, which the parser forgets), and a `..`
|
|
55
|
-
* segment (which the parser would resolve away) are refused here, reading the
|
|
56
|
-
* value as written.
|
|
57
|
-
*
|
|
58
|
-
* The mount-path half is core's: the path AS WRITTEN — before `URL` has turned
|
|
59
|
-
* a backslash into a slash, resolved a `.` segment away or percent-encoded a
|
|
60
|
-
* character — goes to `AvProtocol.normalizeEntrypoint`, the rule the server's
|
|
61
|
-
* config resolution applies, so the two halves of F-822 cannot disagree
|
|
62
|
-
* (`entrypoint.parity.spec.ts`). Only its slash spelling is coerced — `api`,
|
|
63
|
-
* `/api/` and `//api//` are one intent, `/api` — and root is `""`.
|
|
64
|
-
*
|
|
65
|
-
* @throws ClientConfigError in one sentence that does not echo the value.
|
|
66
|
-
*/
|
|
67
44
|
export function resolveEntrypoint(value) {
|
|
68
45
|
if (/\s/u.test(value)) {
|
|
69
46
|
throw new ClientConfigError(`entrypoint contains whitespace; ${ENTRYPOINT_IS}, written without spaces, tabs or line breaks.`);
|
|
@@ -86,8 +63,6 @@ export function resolveEntrypoint(value) {
|
|
|
86
63
|
if (value.includes("?") || value.includes("#")) {
|
|
87
64
|
throw new ClientConfigError("entrypoint must not carry a query or fragment; it is the origin plus mount path only.");
|
|
88
65
|
}
|
|
89
|
-
// Over the whole URL, authority included — wider than the mount-path rule,
|
|
90
|
-
// which refuses a `..` too — so a `..` anywhere keeps its shipped message.
|
|
91
66
|
if (value.split(/[/\\]/u).some(isDotDotSegment)) {
|
|
92
67
|
throw new ClientConfigError(`entrypoint's mount path has a .. segment; ${ENTRYPOINT_IS}, with no dot segments.`);
|
|
93
68
|
}
|
|
@@ -99,28 +74,14 @@ export function resolveEntrypoint(value) {
|
|
|
99
74
|
deployment.pathname = "/";
|
|
100
75
|
return { deployment, path: normalized.path };
|
|
101
76
|
}
|
|
102
|
-
/**
|
|
103
|
-
* The mount path of an http(s) URL exactly as written: everything after the
|
|
104
|
-
* scheme, the slashes that follow it, and the authority. The authority ends
|
|
105
|
-
* where WHATWG URL parsing ends it for a special scheme — at the first `/`,
|
|
106
|
-
* `\`, `?` or `#` — so this is the text the parser turns into `pathname`,
|
|
107
|
-
* before it does.
|
|
108
|
-
*/
|
|
109
77
|
function writtenMountPath(value) {
|
|
110
78
|
return value.replace(/^[A-Za-z][A-Za-z0-9+.-]*:[/\\]*[^/\\?#]*/u, "");
|
|
111
79
|
}
|
|
112
|
-
/**
|
|
113
|
-
* The entrypoint as one absolute URL with no trailing slash — the form the
|
|
114
|
-
* emitted transport joins its routes to (§12.1), and the default a generated
|
|
115
|
-
* client embeds (§15.2, Q5: `generated/metadata.ts`'s `DEFAULT_ENTRYPOINT`).
|
|
116
|
-
* Root is the bare origin.
|
|
117
|
-
*/
|
|
118
80
|
export function entrypointHref(entrypoint) {
|
|
119
81
|
const url = new URL(entrypoint.deployment.href);
|
|
120
82
|
url.pathname = entrypoint.path === "" ? "/" : entrypoint.path;
|
|
121
83
|
return url.href.replace(/\/$/, "");
|
|
122
84
|
}
|
|
123
|
-
/** `..`, spelled as WHATWG URL resolution recognises it: `.` or `%2e`, any case. */
|
|
124
85
|
function isDotDotSegment(segment) {
|
|
125
86
|
return /^(?:\.|%2e){2}$/iu.test(segment);
|
|
126
87
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Precedence, highest first: the existing process environment
|
|
5
|
-
* > `.env.<mode>.local` > `.env.<mode>` > `.env.local` > `.env`.
|
|
2
|
+
* Precedence, highest first: the existing process environment >
|
|
3
|
+
* `.env.<mode>.local` > `.env.<mode>` > `.env.local` > `.env`.
|
|
6
4
|
*
|
|
7
5
|
* # Why a returned record, and not `process.loadEnvFile`
|
|
8
6
|
*
|
|
@@ -11,19 +9,19 @@
|
|
|
11
9
|
* mechanism here, for three reasons that were measured rather than argued:
|
|
12
10
|
*
|
|
13
11
|
* 1. It writes into the live `process.env`. A resolution that mutates the global
|
|
14
|
-
* leaks across tests and across invocations in one process; a suite that
|
|
15
|
-
*
|
|
16
|
-
* 2. It reports an unreadable file (`EACCES`) as `ENOENT`. Catching `ENOENT`
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* leaks across tests and across invocations in one process; a suite that passes
|
|
13
|
+
* alone and fails in sequence is exactly that leak.
|
|
14
|
+
* 2. It reports an unreadable file (`EACCES`) as `ENOENT`. Catching `ENOENT` from
|
|
15
|
+
* it would treat a `.env` the user cannot read as a `.env` that is not there —
|
|
16
|
+
* a silent wrong answer.
|
|
19
17
|
* 3. It silently drops lines that are not assignments. So does `util.parseEnv`;
|
|
20
18
|
* neither has a notion of a malformed file.
|
|
21
19
|
*
|
|
22
|
-
* So each file is read with `readFileSync` (whose error code is the truth),
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* So each file is read with `readFileSync` (whose error code is the truth), parsed
|
|
21
|
+
* with `util.parseEnv` (the same parser `loadEnvFile` uses — pinned by a parity
|
|
22
|
+
* test), checked for lines the parser would discard, and folded first-write-wins
|
|
23
|
+
* over the candidates in priority order. Nothing global is read or written: the
|
|
24
|
+
* process environment is an INPUT.
|
|
27
25
|
*/
|
|
28
26
|
/** A resolved environment: names to values, never `undefined`. */
|
|
29
27
|
export type EnvRecord = Readonly<Record<string, string>>;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseEnv } from "node:util";
|
|
4
|
-
/** The mode used when neither an explicit mode nor `NODE_ENV` supplies one. */
|
|
5
4
|
export const DEFAULT_ENV_MODE = "development";
|
|
6
|
-
/** A candidate exists but could not be read, or contains a discarded line. */
|
|
7
5
|
export class EnvFileError extends Error {
|
|
8
6
|
filePath;
|
|
9
7
|
reason;
|
|
@@ -14,7 +12,6 @@ export class EnvFileError extends Error {
|
|
|
14
12
|
this.reason = reason;
|
|
15
13
|
}
|
|
16
14
|
}
|
|
17
|
-
/** The four file candidates for a mode, highest precedence first. */
|
|
18
15
|
export function envCascadeCandidates(mode) {
|
|
19
16
|
return [`.env.${mode}.local`, `.env.${mode}`, ".env.local", ".env"];
|
|
20
17
|
}
|
|
@@ -34,8 +31,6 @@ export function resolveEnvCascade(input) {
|
|
|
34
31
|
env[name] = value;
|
|
35
32
|
}
|
|
36
33
|
const loaded = [];
|
|
37
|
-
// Highest precedence first: under first-write-wins, the first source to
|
|
38
|
-
// define a name owns it. Reversing this loop inverts the precedence.
|
|
39
34
|
for (const candidate of candidates) {
|
|
40
35
|
const filePath = path.join(input.directory, candidate);
|
|
41
36
|
const content = read(filePath);
|
|
@@ -49,7 +44,6 @@ export function resolveEnvCascade(input) {
|
|
|
49
44
|
}
|
|
50
45
|
return { mode, env: Object.freeze(env), candidates, loaded };
|
|
51
46
|
}
|
|
52
|
-
/** Reads a candidate from disk; `ENOENT` alone means "not there". */
|
|
53
47
|
export function readEnvFileFromDisk(filePath) {
|
|
54
48
|
try {
|
|
55
49
|
return readFileSync(filePath, "utf8");
|
|
@@ -61,12 +55,6 @@ export function readEnvFileFromDisk(filePath) {
|
|
|
61
55
|
throw new EnvFileError(filePath, "unreadable", `exists but cannot be read (${code ?? "unknown error"})`);
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Parses one file's content. A file whose content holds a line the parser would
|
|
66
|
-
* silently discard is malformed: the user wrote something they expect to take
|
|
67
|
-
* effect, and it would not. The error names the line number, never its content
|
|
68
|
-
* — a `.env` line is as likely as not to be a secret.
|
|
69
|
-
*/
|
|
70
58
|
export function parseEnvFile(filePath, content) {
|
|
71
59
|
const source = content.startsWith("") ? content.slice(1) : content;
|
|
72
60
|
const discarded = firstDiscardedLine(source);
|
|
@@ -76,13 +64,6 @@ export function parseEnvFile(filePath, content) {
|
|
|
76
64
|
return parseEnv(source);
|
|
77
65
|
}
|
|
78
66
|
const QUOTES = new Set(['"', "'", "`"]);
|
|
79
|
-
/**
|
|
80
|
-
* The 1-based number of the first line `util.parseEnv` would drop, or
|
|
81
|
-
* `undefined`. Mirrors the parser's line model: blank and `#` lines are
|
|
82
|
-
* skipped, an optional `export ` prefix is allowed, and a value opening with a
|
|
83
|
-
* quote runs to the next matching quote anywhere later in the content —
|
|
84
|
-
* spanning lines — or, when there is none, to the end of its own line.
|
|
85
|
-
*/
|
|
86
67
|
export function firstDiscardedLine(content) {
|
|
87
68
|
let offset = 0;
|
|
88
69
|
while (offset < content.length) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TsConfigJsonResolved } from "get-tsconfig";
|
|
2
|
+
import type { ClientModuleStyle, ImportFileExtension, ModuleFormat } from "../emit/module-style.interface.js";
|
|
3
|
+
import { type LocatedTsconfig } from "./tsconfig.locator.js";
|
|
4
|
+
/**
|
|
5
|
+
* How the generated client is spelled for the project that compiles it: inferred
|
|
6
|
+
* from the project's `tsconfig.json` (`tsconfig.locator.ts`) and, under
|
|
7
|
+
* `node16`/`nodenext`, the nearest `package.json`, by Prisma 7.10's
|
|
8
|
+
* `prisma-client` rules, mirrored from its CLI with the generated extension fixed
|
|
9
|
+
* at `ts`:
|
|
10
|
+
*
|
|
11
|
+
* importFileExtension
|
|
12
|
+
* 1. `allowImportingTsExtensions` or `rewriteRelativeImportExtensions` → `ts`;
|
|
13
|
+
* 2. `module` is `commonjs`, or `moduleResolution` is `bundler` (either case) →
|
|
14
|
+
* `""`;
|
|
15
|
+
* 3. otherwise → `js` (Prisma's `ts → js`).
|
|
16
|
+
*
|
|
17
|
+
* moduleFormat
|
|
18
|
+
* 1. `module` is `commonjs` → `cjs`;
|
|
19
|
+
* 2. `module` is `node16` or `nodenext` → the nearest `package.json` above
|
|
20
|
+
* `generateAt`: `"type": "module"` → `esm`; no `package.json`, an unparseable
|
|
21
|
+
* one, or any other `type` → `cjs`;
|
|
22
|
+
* 3. any other `module` → `esm`; no `module` → `esm` (Prisma's fallback, whose
|
|
23
|
+
* `cjs` arm needs a generated `.cts`).
|
|
24
|
+
*
|
|
25
|
+
* Prisma falls back to the generated extension when there is no tsconfig; this
|
|
26
|
+
* generator refuses instead: its client is TypeScript, and a project without a
|
|
27
|
+
* tsconfig is a JavaScript project, not supported yet.
|
|
28
|
+
*/
|
|
29
|
+
/** What the run follows, and where it read it from. */
|
|
30
|
+
export interface ResolvedModuleStyle extends ClientModuleStyle {
|
|
31
|
+
/** The tsconfig the style was inferred from. */
|
|
32
|
+
readonly tsconfig: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ModuleStyleInput {
|
|
35
|
+
/** The absolute `generateAt`. */
|
|
36
|
+
readonly generateAt: string;
|
|
37
|
+
/** The generated entry file, absolute: the file the project must include. */
|
|
38
|
+
readonly entryFile: string;
|
|
39
|
+
/** The config's `tsconfigFile`, already absolute. */
|
|
40
|
+
readonly tsconfigFile?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The style the project's tsconfig asks for.
|
|
44
|
+
*
|
|
45
|
+
* @throws ClientConfigError when there is no tsconfig to follow, or the named
|
|
46
|
+
* `tsconfigFile` cannot be read.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveModuleStyle(input: ModuleStyleInput): ResolvedModuleStyle;
|
|
49
|
+
/** The refusal of a project with no tsconfig: a JavaScript project. */
|
|
50
|
+
export declare function noTsconfigRefusal(generateAt: string): string;
|
|
51
|
+
export declare function importFileExtensionOf(config: TsConfigJsonResolved): ImportFileExtension;
|
|
52
|
+
export declare function moduleFormatOf(tsconfig: LocatedTsconfig, generateAt: string): ModuleFormat;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ClientConfigError } from "./config.resolver.js";
|
|
4
|
+
import { locateTsconfig } from "./tsconfig.locator.js";
|
|
5
|
+
export function resolveModuleStyle(input) {
|
|
6
|
+
const located = locateTsconfig(input);
|
|
7
|
+
if (located === undefined) {
|
|
8
|
+
throw new ClientConfigError(noTsconfigRefusal(input.generateAt));
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
tsconfig: located.path,
|
|
12
|
+
importFileExtension: importFileExtensionOf(located.config),
|
|
13
|
+
moduleFormat: moduleFormatOf(located, input.generateAt),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function noTsconfigRefusal(generateAt) {
|
|
17
|
+
return (`no tsconfig.json was found in ${generateAt} or any directory above it. ` +
|
|
18
|
+
"Aventara's generated client is TypeScript, compiled by your project's own toolchain and written the way " +
|
|
19
|
+
"its tsconfig.json says; JavaScript projects are not supported yet. Add a tsconfig.json to the project " +
|
|
20
|
+
"(`npx tsc --init` writes one), or, if it has one this search does not find, name it with `tsconfigFile` " +
|
|
21
|
+
"in your framework.client config. Nothing was written.");
|
|
22
|
+
}
|
|
23
|
+
export function importFileExtensionOf(config) {
|
|
24
|
+
const options = config.compilerOptions;
|
|
25
|
+
if (options?.allowImportingTsExtensions ||
|
|
26
|
+
options?.rewriteRelativeImportExtensions) {
|
|
27
|
+
return "ts";
|
|
28
|
+
}
|
|
29
|
+
const moduleResolution = options?.moduleResolution?.toLowerCase();
|
|
30
|
+
return options?.module?.toLowerCase() === "commonjs" ||
|
|
31
|
+
moduleResolution === "bundler"
|
|
32
|
+
? ""
|
|
33
|
+
: "js";
|
|
34
|
+
}
|
|
35
|
+
export function moduleFormatOf(tsconfig, generateAt) {
|
|
36
|
+
const module = tsconfig.config.compilerOptions?.module?.toLowerCase();
|
|
37
|
+
if (module === undefined || module === "") {
|
|
38
|
+
return "esm";
|
|
39
|
+
}
|
|
40
|
+
if (module === "commonjs") {
|
|
41
|
+
return "cjs";
|
|
42
|
+
}
|
|
43
|
+
if (module === "node16" || module === "nodenext") {
|
|
44
|
+
return packageTypeFormat(generateAt);
|
|
45
|
+
}
|
|
46
|
+
return "esm";
|
|
47
|
+
}
|
|
48
|
+
function packageTypeFormat(directory) {
|
|
49
|
+
const manifest = nearestPackageJson(directory);
|
|
50
|
+
if (manifest === undefined) {
|
|
51
|
+
return "cjs";
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(manifest, "utf8"))
|
|
55
|
+
.type === "module"
|
|
56
|
+
? "esm"
|
|
57
|
+
: "cjs";
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return "cjs";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function nearestPackageJson(directory) {
|
|
64
|
+
for (let current = path.resolve(directory);;) {
|
|
65
|
+
const candidate = path.join(current, "package.json");
|
|
66
|
+
if (statSync(candidate, { throwIfNoEntry: false })?.isFile() === true) {
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
const parent = path.dirname(current);
|
|
70
|
+
if (parent === current) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
current = parent;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type TsConfigJsonResolved } from "get-tsconfig";
|
|
2
|
+
/**
|
|
3
|
+
* Which `tsconfig.json` the generated client is spelled for
|
|
4
|
+
* (`module-style.resolver.ts`): the project's own, found the way Prisma 7's
|
|
5
|
+
* `prisma-client` generator finds it — `get-tsconfig`'s `getTsconfig`, the very
|
|
6
|
+
* library and version Prisma 7.10 bundles, searching from the output directory
|
|
7
|
+
* up, `extends` chains merged — with one step Prisma does not take.
|
|
8
|
+
*
|
|
9
|
+
* # A solution-style root (Vite's layout)
|
|
10
|
+
*
|
|
11
|
+
* `npm create vite` writes a `tsconfig.json` that compiles nothing — `"files":
|
|
12
|
+
* []` and `references` to `tsconfig.app.json` and `tsconfig.node.json` — so the
|
|
13
|
+
* nearest config names no compiler option at all. When the config found does
|
|
14
|
+
* not include the generated entry file and has `references`, the referenced
|
|
15
|
+
* project that does include it is the one used, the first in `references`
|
|
16
|
+
* order — how TypeScript's own language service picks a file's project. With
|
|
17
|
+
* none including it, the root stays the answer, as it is Prisma's.
|
|
18
|
+
*
|
|
19
|
+
* # `tsconfigFile`
|
|
20
|
+
*
|
|
21
|
+
* The escape hatch for a layout this discovery gets wrong (a monorepo, a
|
|
22
|
+
* non-standard name): the config's `tsconfigFile`, resolved against the config
|
|
23
|
+
* file's directory, used as named — no search, no reference step. A missing or
|
|
24
|
+
* unreadable one is refused, naming the path it resolved to.
|
|
25
|
+
*/
|
|
26
|
+
/** The tsconfig a run follows: its path and its resolved content (`extends` merged). */
|
|
27
|
+
export interface LocatedTsconfig {
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly config: TsConfigJsonResolved;
|
|
30
|
+
}
|
|
31
|
+
export interface TsconfigLocation {
|
|
32
|
+
/** The absolute `generateAt`: the search starts there. */
|
|
33
|
+
readonly generateAt: string;
|
|
34
|
+
/** The generated entry file the project must include, absolute. */
|
|
35
|
+
readonly entryFile: string;
|
|
36
|
+
/** The config's `tsconfigFile`, already absolute; the search otherwise. */
|
|
37
|
+
readonly tsconfigFile?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The tsconfig `location` names, or `undefined` when no `tsconfig.json` is in
|
|
41
|
+
* `generateAt` or any directory above it (and none was named).
|
|
42
|
+
*
|
|
43
|
+
* @throws ClientConfigError when the named `tsconfigFile` is missing or unreadable.
|
|
44
|
+
*/
|
|
45
|
+
export declare function locateTsconfig(location: TsconfigLocation): LocatedTsconfig | undefined;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createFilesMatcher, getTsconfig, parseTsconfig, } from "get-tsconfig";
|
|
4
|
+
import { ClientConfigError } from "./config.resolver.js";
|
|
5
|
+
export function locateTsconfig(location) {
|
|
6
|
+
if (location.tsconfigFile !== undefined) {
|
|
7
|
+
return namedTsconfig(location.tsconfigFile);
|
|
8
|
+
}
|
|
9
|
+
const found = getTsconfig(location.generateAt);
|
|
10
|
+
if (found === null) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
if (found.config.references === undefined ||
|
|
14
|
+
createFilesMatcher(found)(location.entryFile) !== undefined) {
|
|
15
|
+
return found;
|
|
16
|
+
}
|
|
17
|
+
for (const reference of found.config.references) {
|
|
18
|
+
const referenced = referencedTsconfig(found.path, reference.path);
|
|
19
|
+
if (referenced !== undefined &&
|
|
20
|
+
createFilesMatcher(referenced)(location.entryFile) !== undefined) {
|
|
21
|
+
return referenced;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
function namedTsconfig(file) {
|
|
27
|
+
try {
|
|
28
|
+
readFileSync(file, "utf8");
|
|
29
|
+
return { path: file, config: parseTsconfig(file) };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const reason = error instanceof Error && "code" in error
|
|
33
|
+
? String(error.code)
|
|
34
|
+
: error instanceof Error
|
|
35
|
+
? error.message
|
|
36
|
+
: String(error);
|
|
37
|
+
throw new ClientConfigError(`tsconfigFile names ${file}, which could not be read (${reason}); it must name the tsconfig.json your project compiles the generated client with.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function referencedTsconfig(from, reference) {
|
|
41
|
+
const target = path.resolve(path.dirname(from), reference);
|
|
42
|
+
for (const candidate of [target, path.join(target, "tsconfig.json")]) {
|
|
43
|
+
try {
|
|
44
|
+
readFileSync(candidate, "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
return { path: candidate, config: parseTsconfig(candidate) };
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|