@zh-moody/safe-env 0.1.0

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 ADDED
@@ -0,0 +1,127 @@
1
+ # safe-env 🔒
2
+
3
+ **告别 `undefined`!在应用启动的第一行,拦截所有错误配置。**
4
+
5
+ 无论你在写 Vue、React 还是 Node.js,环境变量配置永远是 Bug 的温床。`safe-env` 通过强类型 Schema 校验,确保你的应用在拥有正确配置的前提下才启动。
6
+
7
+ ---
8
+
9
+ ### 🚀 核心特性
10
+
11
+ - **类型安全**:自动推断配置对象类型,拥有完美的 IDE 补全。
12
+ - **链式校验**:内置 `min`, `max`, `validate`, `enum` 等校验逻辑。
13
+ - **自动前缀**:支持 `VITE_` 或 `REACT_APP_` 自动补全,代码里用 `PORT`,配置里用 `VITE_PORT`。
14
+ - **跨端兼容**:自动识别 Node/浏览器环境,支持双端“防御性退出”。
15
+ - **极致轻量**:Minified 体积约 **2.6 KB**,Gzip 后仅 **1.3 KB**,零运行时依赖。
16
+
17
+ ---
18
+
19
+ ### 🚀 快速上手
20
+
21
+ #### 🔹 [Vite / React / Vue] 端使用
22
+ 在前端,环境变量由构建工具(如 Vite)注入到 `import.meta.env` 中。
23
+
24
+ **1. 准备 `.env` 文件:**
25
+ ```bash
26
+ VITE_API_URL=https://api.com
27
+ VITE_PORT=3000
28
+ ```
29
+
30
+ **2. 定义配置 (`src/env.ts`):**
31
+ ```typescript
32
+ import { safeEnv, s } from 'safe-env';
33
+
34
+ export const config = safeEnv({
35
+ apiUrl: s.string().from('VITE_API_URL'), // 别名映射
36
+ port: s.number(3000).from('VITE_PORT'),
37
+ }, {
38
+ source: import.meta.env // ⚠️ 浏览器端必须手动传入数据源
39
+ });
40
+ ```
41
+
42
+ ---
43
+
44
+ #### 🔸 [Node.js / 服务端] 端使用
45
+ 在后端,库会自动寻找并解析磁盘上的 `.env` 文件。
46
+
47
+ **1. 准备 `.env` 文件:**
48
+ ```bash
49
+ DB_HOST=localhost
50
+ DB_PORT=5432
51
+ ```
52
+
53
+ **2. 定义配置 (`src/db.ts`):**
54
+ ```typescript
55
+ import { safeEnv, s } from 'safe-env';
56
+
57
+ const dbConfig = safeEnv({
58
+ DB_HOST: s.string('localhost'),
59
+ DB_PORT: s.number(5432)
60
+ }); // ⚠️ Node 端无需传 source,会自动读取文件
61
+
62
+ export default dbConfig;
63
+ ```
64
+
65
+ ---
66
+
67
+ ### 📂 核心对比:Schema 与 .env 对应关系
68
+
69
+ | Schema 定义 | .env 中的写法 | 结果 |
70
+ | :--- | :--- | :--- |
71
+ | `s.string()` | `KEY=val` | ✅ 通过 |
72
+ | `s.string()` | *(未填写)* | ❌ **报错并拦截启动** |
73
+ | `s.string('App')` | `KEY=` (留空) | ✅ 自动降级为 `'App'` |
74
+ | `s.number()` | `KEY=abc` | ❌ **报错:Invalid number** |
75
+
76
+ ---
77
+
78
+ ### 🛠️ API 详解
79
+
80
+ #### 1. 定义字段 (`s.xxx`)
81
+ - `s.string(default?)`: 字符串。若无默认值则必填。
82
+ - `s.number(default?)`: 数字。自动将字符串转为 `number`。
83
+ - `s.boolean(default?)`: 布尔。将 `"true"` 解析为 `true`。
84
+ - `s.enum(options, default?)`: 枚举。值必须在数组中。
85
+ - 示例:`s.enum(['dev', 'prod'], 'dev')`
86
+
87
+ #### 2. 增强校验 (链式调用)
88
+ 每个通过 `s` 定义的字段都可以调用以下方法进行增强:
89
+
90
+ - **`.from(key)`**: 指定环境变量名。
91
+ ```typescript
92
+ // 即使变量叫 VITE_PATH,代码里也可以叫 port
93
+ port: s.number().from('VITE_PATH')
94
+ ```
95
+
96
+ - **`.min(n)` / `.max(n)`**: 限制数字取值范围。
97
+ ```typescript
98
+ // 端口必须在 1-65535 之间
99
+ PORT: s.number().min(1).max(65535)
100
+ ```
101
+
102
+ - **`.validate(fn, msg?)`**: 自定义校验(如:邮箱、URL 格式)。
103
+ ```typescript
104
+ // 必须是安全地址
105
+ API: s.string().validate(v => v.startsWith('https'), 'Must be HTTPS')
106
+ ```
107
+
108
+ #### 3. 加载选项 (`SafeEnvOptions`)
109
+
110
+ | 选项 | 适用环境 | 说明 |
111
+ | :--- | :--- | :--- |
112
+ | `source` | **[Vite/浏览器]** | **必填**。传入 `import.meta.env` 或 `process.env`。 |
113
+ | `prefix` | **通用** | **可选**。自动补全前缀,如 `prefix: 'VITE_'`。 |
114
+ | `mode` | **[Node.js]** | **可选**。指定模式(dev/prod)以加载对应的 `.env.prod` 等文件。 |
115
+ | `loadProcessEnv` | **[Node.js]** | **可选**。是否加载系统环境变量,默认 `true`。 |
116
+
117
+ ---
118
+
119
+ ### 🎨 错误报告:长什么样?
120
+
121
+ 当校验失败时,`safe-env` 会在控制台打印一个显眼的表格,告诉你哪个字段错了、原因是什么以及当前的值。在浏览器中这会抛出一个 Error 阻止应用启动,在 Node.js 中会直接 `process.exit(1)`。
122
+
123
+ ---
124
+
125
+ ### 📦 跨平台支持
126
+ - **前端**: Vite (Vue/React/Vanilla)。
127
+ - **后端**: Node.js (ESM/CJS)。
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var _fs = require('fs'); var _fs2 = _interopRequireDefault(_fs);var _path = require('path'); var _path2 = _interopRequireDefault(_path);function s(i){let r={},c=i.split(/\r?\n/);for(let f of c){let n=f.trim();if(!n||n.startsWith("#"))continue;let e=n.indexOf("=");if(e==-1)continue;let l=n.slice(0,e).trim(),t=n.slice(e+1).trim();(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1)),r[l]=t}return r}function a(i=".env"){try{let r=_path2.default.resolve(process.cwd(),i);if(_fs2.default.existsSync(r))return s(_fs2.default.readFileSync(r,"utf-8"))}catch (e2){}return{}}exports.a = s; exports.b = a;
@@ -0,0 +1 @@
1
+ import o from"fs";import d from"path";function s(i){let r={},c=i.split(/\r?\n/);for(let f of c){let n=f.trim();if(!n||n.startsWith("#"))continue;let e=n.indexOf("=");if(e==-1)continue;let l=n.slice(0,e).trim(),t=n.slice(e+1).trim();(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1)),r[l]=t}return r}function a(i=".env"){try{let r=d.resolve(process.cwd(),i);if(o.existsSync(r))return s(o.readFileSync(r,"utf-8"))}catch{}return{}}export{s as a,a as b};
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});function t(n=".env"){return{}}exports.loadDotEnv = t;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 浏览器端不需要读取文件,所以返回空对象
3
+ */
4
+ declare function loadDotEnv(_filePath?: string): Record<string, string>;
5
+
6
+ export { loadDotEnv };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 浏览器端不需要读取文件,所以返回空对象
3
+ */
4
+ declare function loadDotEnv(_filePath?: string): Record<string, string>;
5
+
6
+ export { loadDotEnv };
@@ -0,0 +1 @@
1
+ function t(n=".env"){return{}}export{t as loadDotEnv};
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunk5ZP5OWCLcjs = require('./chunk-5ZP5OWCL.cjs');exports.loadDotEnv = _chunk5ZP5OWCLcjs.b;
@@ -0,0 +1,3 @@
1
+ declare function loadDotEnv(filePath?: string): Record<string, string>;
2
+
3
+ export { loadDotEnv };
@@ -0,0 +1,3 @@
1
+ declare function loadDotEnv(filePath?: string): Record<string, string>;
2
+
3
+ export { loadDotEnv };
@@ -0,0 +1 @@
1
+ import{b as a}from"./chunk-BEFKPDEA.js";export{a as loadDotEnv};
package/dist/index.cjs ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});var _chunk5ZP5OWCLcjs = require('./chunk-5ZP5OWCL.cjs');function u(r,e,n,s=[]){return{type:r,default:e,required:e===void 0,parse:n,metadata:s.length?{options:s}:{},from(o){return this.sourceKey=o,this},validate(o,m="Custom validation failed"){return this.metadata={...this.metadata,validate:{fn:o,message:m}},this},min(o){return this.metadata={...this.metadata,min:o},this},max(o){return this.metadata={...this.metadata,max:o},this}}}var x={string:r=>u("string",r,e=>String(e)),number:r=>u("number",r,e=>{let n=Number(e);if(isNaN(n))throw new Error(`Invalid number: ${e}`);return n}),boolean:r=>u("boolean",r,e=>e==="true"||e===!0),enum:(r,e)=>u("enum",e,n=>{if(!r.includes(n))throw new Error(`Value "${n}" is not one of: ${r.join(", ")}`);return n},r)};function h(r){let e={r:"\x1B[31m",g:"\x1B[32m",y:"\x1B[33m",b:"\x1B[1m",res:"\x1B[0m",d:"\x1B[2m"};console.error(`
2
+ ${e.r}${e.b}\u274C SafeEnv Error${e.res}
3
+ ${e.d}${"".padEnd(40,"-")}${e.res}`),r.forEach(n=>{let s=n.value===void 0?"undefined":`"${n.value}"`;console.error(`${e.y}${n.key}${e.res} ${e.r}${n.error}${e.res} ${e.d}${s}${e.res}`)}),console.error(`${e.d}${"".padEnd(40,"-")}${e.res}
4
+ ${e.g}\u{1F4A1} Check your .env files.${e.res}
5
+ `)}function D(r,e={}){let{loadProcessEnv:n=!0,source:s,prefix:l=""}=e,o;if(s)o=s;else{let i=e.mode||(typeof process<"u"?process.env.NODE_ENV:"development"),a=[".env",".env.local",`.env.${i}`,`.env.${i}.local`],f={};for(let d of a)f={...f,..._chunk5ZP5OWCLcjs.b.call(void 0, d)};o={...f,...n&&typeof process<"u"?process.env:{}}}let m={},c=[];for(let i in r){let a=r[i],f=a.sourceKey||(o[l+i]!==void 0?l+i:i),d=o[f];try{let t;if(d===void 0||d===""&&a.default!==void 0){if(a.required&&d===void 0)throw new Error("Required field missing");t=a.default}else t=a.parse(d);if(t!==void 0&&a.metadata){let{min:p,max:E,validate:v}=a.metadata;if(typeof t=="number"){if(p!==void 0&&t<p)throw new Error(`Below min ${p}`);if(E!==void 0&&t>E)throw new Error(`Above max ${E}`)}if(v&&!v.fn(t))throw new Error(v.message)}m[i]=t}catch(t){c.push({key:f,error:t.message,value:d})}}if(c.length>0)if(h(c),typeof process<"u"&&!!process.exit&&!s)process.exit(1);else throw new Error("SafeEnv: Configuration validation failed.");return m}exports.loadDotEnv = _chunk5ZP5OWCLcjs.b; exports.parseDotEnv = _chunk5ZP5OWCLcjs.a; exports.reportErrors = h; exports.s = x; exports.safeEnv = D;
@@ -0,0 +1,56 @@
1
+ export { loadDotEnv } from './fs-node.cjs';
2
+
3
+ type BaseType = "string" | "number" | "boolean" | "enum";
4
+ interface FieldDefinition<T = any> {
5
+ type: BaseType;
6
+ default?: T;
7
+ required: boolean;
8
+ sourceKey?: string;
9
+ metadata?: {
10
+ min?: number;
11
+ max?: number;
12
+ options?: T[];
13
+ validate?: {
14
+ fn: (val: T) => boolean;
15
+ message: string;
16
+ };
17
+ };
18
+ parse: (val: any) => T;
19
+ from: (key: string) => FieldDefinition<T>;
20
+ validate: (fn: (val: T) => boolean, message?: string) => FieldDefinition<T>;
21
+ min: (val: number) => FieldDefinition<T>;
22
+ max: (val: number) => FieldDefinition<T>;
23
+ }
24
+ type Schema = Record<string, FieldDefinition>;
25
+ interface EnvError {
26
+ key: string;
27
+ error: string;
28
+ value: any;
29
+ }
30
+ type InferSchema<T> = {
31
+ [K in keyof T]: T[K] extends FieldDefinition<infer U> ? U : never;
32
+ };
33
+
34
+ declare const s: {
35
+ string: (defaultValue?: string) => FieldDefinition<string>;
36
+ number: (defaultValue?: number) => FieldDefinition<number>;
37
+ boolean: (defaultValue?: boolean) => FieldDefinition<boolean>;
38
+ enum: <T extends string>(options: T[], defaultValue?: T) => FieldDefinition<T>;
39
+ };
40
+
41
+ interface SafeEnvOptions {
42
+ mode?: string;
43
+ loadProcessEnv?: boolean;
44
+ source?: Record<string, any>;
45
+ prefix?: string;
46
+ }
47
+ declare function safeEnv<T extends Schema>(schema: T, options?: SafeEnvOptions): InferSchema<T>;
48
+
49
+ /***
50
+ * 将 .env 内容字符串解析为对象
51
+ * */
52
+ declare function parseDotEnv(content: string): Record<string, string>;
53
+
54
+ declare function reportErrors(errors: EnvError[]): void;
55
+
56
+ export { type BaseType, type EnvError, type FieldDefinition, type InferSchema, type Schema, parseDotEnv, reportErrors, s, safeEnv };
@@ -0,0 +1,56 @@
1
+ export { loadDotEnv } from './fs-node.js';
2
+
3
+ type BaseType = "string" | "number" | "boolean" | "enum";
4
+ interface FieldDefinition<T = any> {
5
+ type: BaseType;
6
+ default?: T;
7
+ required: boolean;
8
+ sourceKey?: string;
9
+ metadata?: {
10
+ min?: number;
11
+ max?: number;
12
+ options?: T[];
13
+ validate?: {
14
+ fn: (val: T) => boolean;
15
+ message: string;
16
+ };
17
+ };
18
+ parse: (val: any) => T;
19
+ from: (key: string) => FieldDefinition<T>;
20
+ validate: (fn: (val: T) => boolean, message?: string) => FieldDefinition<T>;
21
+ min: (val: number) => FieldDefinition<T>;
22
+ max: (val: number) => FieldDefinition<T>;
23
+ }
24
+ type Schema = Record<string, FieldDefinition>;
25
+ interface EnvError {
26
+ key: string;
27
+ error: string;
28
+ value: any;
29
+ }
30
+ type InferSchema<T> = {
31
+ [K in keyof T]: T[K] extends FieldDefinition<infer U> ? U : never;
32
+ };
33
+
34
+ declare const s: {
35
+ string: (defaultValue?: string) => FieldDefinition<string>;
36
+ number: (defaultValue?: number) => FieldDefinition<number>;
37
+ boolean: (defaultValue?: boolean) => FieldDefinition<boolean>;
38
+ enum: <T extends string>(options: T[], defaultValue?: T) => FieldDefinition<T>;
39
+ };
40
+
41
+ interface SafeEnvOptions {
42
+ mode?: string;
43
+ loadProcessEnv?: boolean;
44
+ source?: Record<string, any>;
45
+ prefix?: string;
46
+ }
47
+ declare function safeEnv<T extends Schema>(schema: T, options?: SafeEnvOptions): InferSchema<T>;
48
+
49
+ /***
50
+ * 将 .env 内容字符串解析为对象
51
+ * */
52
+ declare function parseDotEnv(content: string): Record<string, string>;
53
+
54
+ declare function reportErrors(errors: EnvError[]): void;
55
+
56
+ export { type BaseType, type EnvError, type FieldDefinition, type InferSchema, type Schema, parseDotEnv, reportErrors, s, safeEnv };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import{a as b,b as $}from"./chunk-BEFKPDEA.js";function u(r,e,n,s=[]){return{type:r,default:e,required:e===void 0,parse:n,metadata:s.length?{options:s}:{},from(o){return this.sourceKey=o,this},validate(o,m="Custom validation failed"){return this.metadata={...this.metadata,validate:{fn:o,message:m}},this},min(o){return this.metadata={...this.metadata,min:o},this},max(o){return this.metadata={...this.metadata,max:o},this}}}var x={string:r=>u("string",r,e=>String(e)),number:r=>u("number",r,e=>{let n=Number(e);if(isNaN(n))throw new Error(`Invalid number: ${e}`);return n}),boolean:r=>u("boolean",r,e=>e==="true"||e===!0),enum:(r,e)=>u("enum",e,n=>{if(!r.includes(n))throw new Error(`Value "${n}" is not one of: ${r.join(", ")}`);return n},r)};function h(r){let e={r:"\x1B[31m",g:"\x1B[32m",y:"\x1B[33m",b:"\x1B[1m",res:"\x1B[0m",d:"\x1B[2m"};console.error(`
2
+ ${e.r}${e.b}\u274C SafeEnv Error${e.res}
3
+ ${e.d}${"".padEnd(40,"-")}${e.res}`),r.forEach(n=>{let s=n.value===void 0?"undefined":`"${n.value}"`;console.error(`${e.y}${n.key}${e.res} ${e.r}${n.error}${e.res} ${e.d}${s}${e.res}`)}),console.error(`${e.d}${"".padEnd(40,"-")}${e.res}
4
+ ${e.g}\u{1F4A1} Check your .env files.${e.res}
5
+ `)}function D(r,e={}){let{loadProcessEnv:n=!0,source:s,prefix:l=""}=e,o;if(s)o=s;else{let i=e.mode||(typeof process<"u"?process.env.NODE_ENV:"development"),a=[".env",".env.local",`.env.${i}`,`.env.${i}.local`],f={};for(let d of a)f={...f,...$(d)};o={...f,...n&&typeof process<"u"?process.env:{}}}let m={},c=[];for(let i in r){let a=r[i],f=a.sourceKey||(o[l+i]!==void 0?l+i:i),d=o[f];try{let t;if(d===void 0||d===""&&a.default!==void 0){if(a.required&&d===void 0)throw new Error("Required field missing");t=a.default}else t=a.parse(d);if(t!==void 0&&a.metadata){let{min:p,max:E,validate:v}=a.metadata;if(typeof t=="number"){if(p!==void 0&&t<p)throw new Error(`Below min ${p}`);if(E!==void 0&&t>E)throw new Error(`Above max ${E}`)}if(v&&!v.fn(t))throw new Error(v.message)}m[i]=t}catch(t){c.push({key:f,error:t.message,value:d})}}if(c.length>0)if(h(c),typeof process<"u"&&!!process.exit&&!s)process.exit(1);else throw new Error("SafeEnv: Configuration validation failed.");return m}export{$ as loadDotEnv,b as parseDotEnv,h as reportErrors,x as s,D as safeEnv};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@zh-moody/safe-env",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "author": "Moody",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "browser": {
11
+ "./dist/fs-node.js": "./dist/fs-browser.js",
12
+ "./dist/fs-node.cjs": "./dist/fs-browser.js"
13
+ },
14
+ "scripts": {
15
+ "dev": "tsup src/index.ts src/fs-node.ts src/fs-browser.ts --watch",
16
+ "build": "tsup src/index.ts src/fs-node.ts src/fs-browser.ts --format cjs,esm --dts --minify --clean --splitting",
17
+ "test": "vitest run"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.cjs"
24
+ }
25
+ },
26
+ "keywords": [],
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "devDependencies": {
31
+ "tsup": "^8.5.1",
32
+ "typescript": "^5.9.3",
33
+ "vitest": "^4.1.2"
34
+ }
35
+ }