Skip to content

Commit f1863fb

Browse files
committed
♻️ 重构了env的实现,提供了defineSafeEnv,可以和zod搭配使用
1 parent a53c70c commit f1863fb

File tree

2 files changed

+78
-116
lines changed

2 files changed

+78
-116
lines changed

node/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@gaubee/node",
3-
"version": "0.2.4",
3+
"version": "0.3.0",
44
"exports": {
55
"./env": "./src/env.ts",
66
"./path": "./src/path.ts",

node/src/env.ts

Lines changed: 77 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {func_remember, obj_assign_props, obj_props} from "@gaubee/util";
1+
import {iter_first_not_null} from "@gaubee/util";
22

33
type CustomEnvConfig<T = unknown, Env = unknown> = {
44
default: (env: Env) => T;
@@ -15,15 +15,7 @@ export type DefineEnvChain<P extends string, Env> = Env & {
1515
and<KV2 extends Record<string, EnvConfig<Env>>>(kv: KV2, env?: Record<string, string>): DefineEnvChain<P, Env & DefineEnv<P, KV2>>;
1616
end(): Env;
1717
};
18-
export const viteEnvSource = () =>
19-
new Proxy((import.meta as any).env, {
20-
get(target, key, receiver) {
21-
return Reflect.get(target, `VITE_${key as string}`, receiver);
22-
},
23-
set(target, key, value, receiver) {
24-
return Reflect.set(target, `VITE_${key as string}`, value, receiver);
25-
},
26-
});
18+
2719
declare const process: any;
2820
export const nodeEnvSource = () => process.env;
2921
declare const Deno: any;
@@ -42,123 +34,93 @@ export const denoEnvSource = () =>
4234
);
4335
declare const Bun: any;
4436
export const bunEnvSource = () => Bun.env;
45-
export const storageEnvSource = (storage: Storage = sessionStorage) =>
37+
export const storageEnvSource = (storages: Storage[] = [sessionStorage, localStorage]) =>
4638
new Proxy(
4739
{},
4840
{
4941
get(_target, key) {
50-
return storage.getItem(key as string);
42+
for (const storage of storages) {
43+
const res = storage.getItem(key as string);
44+
if (res != null) {
45+
return res;
46+
}
47+
}
5148
},
5249
set(_target, key, value) {
53-
storage.setItem(key as string, value as string);
54-
return true;
50+
for (const storage of storages) {
51+
storage.setItem(key as string, value as string);
52+
return true;
53+
}
54+
return false;
5555
},
5656
},
5757
);
5858
export const autoEnvSource = (fallback: () => Record<string, string> = () => ({})): Record<string, string | undefined> => {
59-
return typeof Deno != "undefined"
60-
? denoEnvSource()
61-
: typeof Bun != "undefined"
62-
? bunEnvSource()
63-
: typeof (import.meta as any).env != "undefined"
64-
? viteEnvSource()
65-
: typeof process != "undefined"
66-
? nodeEnvSource()
67-
: typeof sessionStorage != "undefined"
68-
? storageEnvSource(sessionStorage)
69-
: fallback();
59+
return (
60+
iter_first_not_null(
61+
[
62+
[typeof Deno != "undefined", denoEnvSource],
63+
[typeof Bun != "undefined", bunEnvSource],
64+
[typeof process != "undefined", nodeEnvSource],
65+
[typeof sessionStorage != "undefined", storageEnvSource],
66+
] as const,
67+
([match, fn]) => (match ? fn() : null),
68+
) ?? fallback()
69+
);
7070
};
7171

72-
export const defineEnv = <P extends string, KV extends Record<string, EnvConfig>>(
73-
prefix: P,
74-
kv: KV,
75-
source = autoEnvSource(),
76-
ext: object = {},
77-
): DefineEnvChain<P, DefineEnv<P, KV>> => {
78-
const prefix_up = prefix.toUpperCase();
79-
const res = obj_assign_props(ext, {
80-
and(kv, env) {
81-
return defineEnv(prefix, kv as any, env, res);
82-
},
83-
end() {
84-
const closed_res = obj_assign_props(res, {and: undefined, end: undefined}) as any;
85-
delete closed_res.and;
86-
delete closed_res.end;
87-
return closed_res;
88-
},
89-
} as DefineEnvChain<P, DefineEnv<P, KV>>);
90-
for (const key of obj_props(kv, {excludeSymbols: true})) {
91-
const ENV_KEY = `${prefix_up}_${key.toUpperCase()}` as keyof DefineEnv<P, KV>;
92-
const env_value = kv[key];
93-
let envConfig: CustomEnvConfig<any>;
94-
switch (typeof env_value) {
95-
case "string":
96-
envConfig = {
97-
default: () => env_value,
98-
stringify: (v) => v,
99-
parse: (v) => v,
100-
} satisfies CustomEnvConfig<string>;
101-
break;
102-
case "number":
103-
envConfig = {
104-
default: () => env_value,
105-
stringify: (v) => `${v}`,
106-
parse: (v) => +v,
107-
} satisfies CustomEnvConfig<number>;
108-
break;
109-
case "bigint":
110-
envConfig = {
111-
default: env_value,
112-
stringify: (v) => `${v}`,
113-
parse: (v) => BigInt(v),
114-
} satisfies CustomEnvConfig<bigint>;
115-
break;
116-
case "boolean":
117-
envConfig = {
118-
default: () => env_value,
119-
stringify: (v) => `${v}`,
120-
parse: (v) => !(v === "false" || v === ""),
121-
} satisfies CustomEnvConfig<boolean>;
122-
break;
123-
case "object":
124-
envConfig = env_value;
125-
break;
126-
case "function":
127-
envConfig = {
128-
default: env_value,
129-
stringify: (v) => v,
130-
parse: (v) => v,
131-
} satisfies CustomEnvConfig<string>;
132-
break;
133-
default:
134-
throw new Error(`unkonwn type for key:${key}`);
135-
}
72+
type Parser = {parse: (v: string | undefined) => unknown} | ((v: string | undefined) => unknown);
73+
type ParserReturn<T> = T extends {parse: (v: string | undefined) => infer R} ? R : T extends (v: string | undefined) => infer R ? R : unknown;
13674

137-
const getter = func_remember(
138-
(v) => {
139-
return envConfig.parse(v, res);
140-
},
141-
(v) => v,
142-
);
143-
const setter = func_remember(
144-
(v) => {
145-
source[ENV_KEY] = envConfig.stringify(v, res);
146-
},
147-
(v) => v,
148-
);
75+
type SaveEnvReturn<T extends Record<string, Parser>> = {
76+
[K in keyof T]: T[K] extends Parser ? ParserReturn<T[K]> : never;
77+
};
14978

150-
obj_assign_props(res, {
151-
get [ENV_KEY]() {
152-
const val = source[ENV_KEY];
153-
if (typeof val === "string") {
154-
return getter(val);
155-
}
156-
return envConfig.default(res);
157-
},
158-
set [ENV_KEY](v: string) {
159-
setter(v);
160-
},
161-
});
162-
}
163-
return res;
79+
/**
80+
* @example
81+
* ```ts
82+
* const safeEnv = defineSafeEnv({
83+
* // use zod
84+
* STR: z.string().default("123"),
85+
* NUM: z.number().default(456),
86+
*
87+
* // use custom parser
88+
* BINT: (value) => BigInt(String(value ?? "0")),
89+
* });
90+
*
91+
* const r1 = safeEnv(); // will auto use runtime env: support bun/deno/node/vite/browser(sessionStorage+localStorage)
92+
* r1.STR; // safe type: string
93+
* r1.NUM; // safe type: number
94+
* r1.BINT; // safe type: bigint
95+
*
96+
* console.log(r1); // { STR: "114514", NUM: 123, BINT: 0n }
97+
*
98+
* const r2 = safeEnv({STR: "qqq"});
99+
* console.log(r2); // { STR: "114514", NUM: 123, BINT: 0n }
100+
* ```
101+
* @param envShape
102+
* @returns
103+
*/
104+
export const defineSafeEnv = <ENV extends Record<string, Parser>>(envShape: ENV) => {
105+
return (source: Record<string, string | undefined> = autoEnvSource()): SaveEnvReturn<ENV> => {
106+
const env: any = {};
107+
for (const prop in envShape) {
108+
const parser = envShape[prop as keyof ENV];
109+
let get: undefined | (() => unknown);
110+
if (typeof parser === "function") {
111+
env[prop] = parser(source[prop as keyof ENV]);
112+
}
113+
if (typeof parser === "object" && parser !== null && typeof parser.parse === "function") {
114+
env[prop] = parser.parse(source[prop as keyof ENV]);
115+
}
116+
if (get) {
117+
Object.defineProperty(env, prop, {
118+
get,
119+
enumerable: true,
120+
configurable: false,
121+
});
122+
}
123+
}
124+
return env as SaveEnvReturn<ENV>;
125+
};
164126
};

0 commit comments

Comments
 (0)