Skip to content

Commit 740a4ce

Browse files
committed
add AppFacade#env(key: string) (#251)
1 parent 97e5a02 commit 740a4ce

7 files changed

Lines changed: 95 additions & 5 deletions

File tree

packages/core/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@rcompat/cli": "^0.18.1",
3030
"@rcompat/crypto": "^0.16.1",
3131
"@rcompat/dict": "^0.5.1",
32+
"@rcompat/env": "^0.17.2",
3233
"@rcompat/error": "^0.3.1",
3334
"@rcompat/fn": "^0.5.1",
3435
"@rcompat/fs": "^0.28.1",
@@ -54,6 +55,11 @@
5455
}
5556
},
5657
"exports": {
58+
"./AppFacade": {
59+
"@primate/source": "./src/public/AppFacade.ts",
60+
"browser": "./lib/private/app/Facade.browser.js",
61+
"default": "./lib/public/AppFacade.js"
62+
},
5763
"./*": {
5864
"@primate/source": "./src/public/*.ts",
5965
"default": "./lib/public/*.js"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { Dict } from "@rcompat/type";
2+
import type { Parsed } from "pema";
3+
4+
type EnvSchema = Dict<Parsed<unknown>>;
5+
6+
export type { EnvSchema as default };
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Browser stub for AppFacade.
3+
* env must never be called in frontend code, ecrets must not leak to clients
4+
*/
5+
export default class AppFacade {
6+
config(_path: string): never {
7+
throw new Error("AppFacade.config() is not available in the browser");
8+
}
9+
10+
env(_key: string): never {
11+
throw new Error(
12+
"AppFacade.env() is server-only. Do not call env() in frontend/browser code.",
13+
);
14+
}
15+
16+
view(_name: string): never {
17+
throw new Error("AppFacade.view() is not available in the browser");
18+
}
19+
20+
get root(): never {
21+
throw new Error("AppFacade.root is not available in the browser");
22+
}
23+
}

packages/core/src/private/app/Facade.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
1-
import type Config from "#config/Config";
1+
import type EnvSchema from "#app/EnvSchema";
22
import type ServerView from "#client/ServerView";
3+
import type Config from "#config/Config";
4+
import E from "#errors";
35
import type ServeApp from "#serve/App";
46
import dict from "@rcompat/dict";
7+
import env from "@rcompat/env";
8+
import type { ObjectType } from "pema";
9+
import ParseError from "pema/ParseError";
510

611
const s_attach = Symbol("attach");
712
const s_config = Symbol("config");
813

914
export { s_attach, s_config };
1015

11-
export default class AppFacade {
16+
type Env<P extends EnvSchema> = { [K in keyof P]: P[K]["infer"] };
17+
18+
export default class AppFacade<T extends EnvSchema = EnvSchema> {
1219
#config: Config;
1320
#app?: ServeApp;
21+
#env?: Env<T>;
1422

1523
constructor(config: Config) {
1624
this.#config = config;
1725
}
1826

1927
[s_attach](app: ServeApp) {
2028
this.#app = app;
29+
30+
const schema = this.#config.env.schema as ObjectType<T> | undefined;
31+
if (schema !== undefined) {
32+
try {
33+
this.#env = schema.coerce(env.toJSON()) as Env<T>;
34+
} catch (error) {
35+
if (ParseError.is(error)) throw E.env_invalid_schema(error);
36+
throw error;
37+
}
38+
}
2139
}
2240

2341
get [s_config]() {
@@ -28,6 +46,15 @@ export default class AppFacade {
2846
return dict.get(this.#config, path);
2947
}
3048

49+
env<K extends keyof T>(key: K): T[K]["infer"];
50+
env(key: string): unknown {
51+
if (this.#env !== undefined) {
52+
if (!(key in this.#env)) throw E.env_missing_key(key);
53+
return this.#env[key as keyof T];
54+
}
55+
return env.get(key);
56+
}
57+
3158
get #with() {
3259
if (!this.#app) throw new Error("ServeApp not bound yet (used too early)");
3360
return this.#app;
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
import type EnvSchema from "#app/EnvSchema";
12
import AppFacade from "#app/Facade";
23
import schema from "#config/schema";
4+
import type { ObjectType } from "pema";
35

4-
export default (input: typeof schema.input = {}) =>
5-
new AppFacade(schema.parse(input));
6-
;
6+
export default function config<P extends EnvSchema = EnvSchema>(
7+
input: typeof schema.input & {
8+
env?: { schema?: ObjectType<P> };
9+
} = {},
10+
): AppFacade<P> {
11+
return new AppFacade<P>(schema.parse(input as typeof schema.input));
12+
}

packages/core/src/private/config/schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type DB from "#db/DB";
22
import type Module from "#Module";
33
import fs from "@rcompat/fs";
4+
import type { Dict } from "@rcompat/type";
5+
import type { ObjectType, Parsed } from "pema";
46
import p from "pema";
57

68
export default p({
@@ -29,6 +31,9 @@ export default p({
2931
blocking: p.boolean.default(true),
3032
}).optional(),
3133
},
34+
env: {
35+
schema: p.pure<ObjectType<Dict<Parsed<unknown>>>>().optional(),
36+
},
3237
modules: p.array(p.object({
3338
name: p.string,
3439
setup: p.function,

packages/core/src/private/errors.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import error from "@rcompat/error";
22
import type { FileRef } from "@rcompat/fs";
3+
import type ParseError from "pema/ParseError";
34

45
const t = error.template;
56

@@ -203,6 +204,21 @@ const VIEW = error.coded({
203204
view_error,
204205
});
205206

207+
function env_invalid_schema(cause: ParseError) {
208+
const issues = cause.issues
209+
.map(i => ` ${i.path.replace(/^\//, "")}: ${i.message}`)
210+
.join("\n");
211+
return t`environment variables failed validation:\n${issues}`;
212+
}
213+
function env_missing_key(key: string) {
214+
return t`environment variable ${key} is not defined`;
215+
}
216+
217+
const ENV = error.coded({
218+
env_invalid_schema,
219+
env_missing_key,
220+
});
221+
206222
const errors = {
207223
...APP,
208224
...BUILD,
@@ -215,6 +231,7 @@ const errors = {
215231
...SESSION,
216232
...TARGET,
217233
...VIEW,
234+
...ENV,
218235
};
219236

220237
export type Code = keyof typeof errors;

0 commit comments

Comments
 (0)