ezcfg 0.2.0 → 0.3.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 CHANGED
@@ -9,8 +9,10 @@ Lite and easy configuration management for Node.js with full TypeScript support.
9
9
 
10
10
  - **Type-safe configuration** - Full TypeScript support with automatic type inference
11
11
  - **Automatic .env file loading** - Loads environment files based on `NODE_ENV`
12
+ - **Direct .env file parsing** - Read from a specific `.env` file without modifying `process.env`
12
13
  - **Validation with clear error messages** - Collects all validation errors at once
13
- - **Zero runtime dependencies** - Only uses `dotenv` for .env file parsing
14
+ - **PostgreSQL config support** - Built-in `postgresConfig` spec via `ezcfg/postgres`
15
+ - **Extensible** - Implement `ConfigSpec` to add custom config types
14
16
  - **Singleton pattern** - Config is lazily evaluated and cached (except in test environment)
15
17
 
16
18
  ## Installation
@@ -90,12 +92,40 @@ const config = getConfig(); // Lazily evaluated and cached
90
92
  | `schema` | `Record<string, unknown>` | Object defining your configuration shape |
91
93
  | `options.loadEnv` | `boolean` | Whether to load .env files automatically (default: `false`) |
92
94
  | `options.envLoader` | `() => void` | Custom function to load environment variables |
95
+ | `options.fromEnvFile` | `string` | Path to a `.env` file to parse directly (does not modify `process.env`) |
93
96
 
94
97
  **Behavior:**
95
98
 
96
99
  - Returns a factory function that lazily evaluates and caches the config
97
100
  - In test environment (`NODE_ENV=test`), config is re-evaluated on each call
98
101
  - Throws `ConfigValidationError` if any required values are missing
102
+ - When `fromEnvFile` is set, the file is parsed into a local object and passed to each spec's `resolve()` — `process.env` is never modified
103
+
104
+ ---
105
+
106
+ ### `parseEnvFile(filePath)`
107
+
108
+ Parses a `.env` file and returns its contents as a `Record<string, string>`. Does not modify `process.env`.
109
+
110
+ ```typescript
111
+ import { parseEnvFile } from 'ezcfg';
112
+
113
+ const env = parseEnvFile('/path/to/.env');
114
+ // { DATABASE_URL: 'postgres://...', API_KEY: 'secret' }
115
+ ```
116
+
117
+ Useful for injecting env vars into test runners:
118
+
119
+ ```typescript
120
+ // vitest.config.ts
121
+ import { parseEnvFile } from 'ezcfg';
122
+
123
+ export default defineConfig({
124
+ test: {
125
+ env: parseEnvFile(resolve(__dirname, '.env')),
126
+ },
127
+ });
128
+ ```
99
129
 
100
130
  ---
101
131
 
@@ -216,6 +246,90 @@ loadEnvFiles({
216
246
  });
217
247
  ```
218
248
 
249
+ ---
250
+
251
+ ## `ezcfg/postgres`
252
+
253
+ PostgreSQL configuration support, available as a subpath import.
254
+
255
+ ```typescript
256
+ import { postgresConfig, PostgresConfig } from 'ezcfg/postgres';
257
+ ```
258
+
259
+ ### `postgresConfig(prefix, mode?)`
260
+
261
+ A `ConfigSpec` implementation for PostgreSQL database configuration. Use with `defineConfig`.
262
+
263
+ ```typescript
264
+ import { defineConfig } from 'ezcfg';
265
+ import { postgresConfig } from 'ezcfg/postgres';
266
+
267
+ const getConfig = defineConfig({
268
+ db: postgresConfig('ORDER_DB'), // reads ORDER_DB_URL
269
+ db2: postgresConfig('PG', 'fields'), // reads PG_HOST, PG_PORT, etc.
270
+ });
271
+
272
+ getConfig().db.host; // "localhost"
273
+ getConfig().db.toString(); // "postgres://..."
274
+ ```
275
+
276
+ **Parameters:**
277
+
278
+ | Parameter | Type | Default | Description |
279
+ |-----------|------|---------|-------------|
280
+ | `prefix` | `string` | — | Environment variable prefix |
281
+ | `mode` | `"url" \| "fields"` | `"url"` | `"url"` reads `{PREFIX}_URL`, `"fields"` reads individual fields |
282
+
283
+ **Fields mode** reads: `{PREFIX}_HOST`, `{PREFIX}_PORT`, `{PREFIX}_DATABASE`, `{PREFIX}_USER`, `{PREFIX}_PASSWORD`
284
+
285
+ ### `PostgresConfig`
286
+
287
+ Immutable value object representing a PostgreSQL connection configuration.
288
+
289
+ ```typescript
290
+ import { PostgresConfig } from 'ezcfg/postgres';
291
+
292
+ // From URL
293
+ const config = PostgresConfig.fromUrl('postgres://user:pass@localhost:5432/mydb');
294
+
295
+ // From environment variables
296
+ const config = PostgresConfig.fromEnv('DATABASE'); // reads DATABASE_URL
297
+ const config = PostgresConfig.fromEnv('PG', { mode: 'fields' });
298
+
299
+ // Builder methods (each returns a new instance)
300
+ config.withDatabase('other_db');
301
+ config.withHost('remote-host');
302
+ config.withPort(5433);
303
+ config.withUser('admin');
304
+ config.withPassword('secret');
305
+ config.withPoolSize(10);
306
+ config.withConnectionTimeout(5000);
307
+ config.withIdleTimeout(30000);
308
+
309
+ // Connection string
310
+ config.toString(); // "postgres://user:pass@localhost:5432/mydb"
311
+ ```
312
+
313
+ ### Using with `fromEnvFile`
314
+
315
+ `postgresConfig` works with `fromEnvFile` — the `.env` file is parsed locally without modifying `process.env`:
316
+
317
+ ```typescript
318
+ import { defineConfig } from 'ezcfg';
319
+ import { postgresConfig } from 'ezcfg/postgres';
320
+
321
+ const getConfig = defineConfig(
322
+ { db: postgresConfig('DATABASE') },
323
+ { fromEnvFile: resolve(__dirname, '.env') }
324
+ );
325
+
326
+ // .env contains: DATABASE_URL=postgres://user@localhost/mydb
327
+ const config = getConfig();
328
+ config.db.database; // "mydb"
329
+ ```
330
+
331
+ ---
332
+
219
333
  ## .env File Loading
220
334
 
221
335
  When `loadEnv: true` is set, ezcfg automatically loads environment files in the following order (later files override earlier ones):
@@ -241,7 +355,7 @@ When `loadEnv: true` is set, ezcfg automatically loads environment files in the
241
355
  When validation fails, ezcfg throws a `ConfigValidationError` with all errors collected:
242
356
 
243
357
  ```typescript
244
- import { defineConfig, env, ConfigValidationError } from 'ezcfg';
358
+ import { defineConfig, env, envNumber, ConfigValidationError } from 'ezcfg';
245
359
 
246
360
  const getConfig = defineConfig({
247
361
  apiKey: env('API_KEY'),
@@ -297,7 +411,7 @@ const config = getConfig();
297
411
  You can also extract the config type for use elsewhere:
298
412
 
299
413
  ```typescript
300
- import { defineConfig, env, type InferConfigType } from 'ezcfg';
414
+ import { defineConfig, env, envNumber, type InferConfigType } from 'ezcfg';
301
415
 
302
416
  const schema = {
303
417
  apiKey: env('API_KEY'),
@@ -315,75 +429,30 @@ function initializeApp(config: AppConfig) {
315
429
  }
316
430
  ```
317
431
 
318
- ## Examples
319
-
320
- ### Database Configuration
321
-
322
- ```typescript
323
- import { defineConfig, env, envNumber, envOptional } from 'ezcfg';
324
-
325
- export const getDbConfig = defineConfig({
326
- host: env('DB_HOST'),
327
- port: envNumber('DB_PORT'),
328
- database: env('DB_NAME'),
329
- user: env('DB_USER'),
330
- password: envOptional('DB_PASSWORD'),
331
- }, { loadEnv: true });
332
-
333
- // Usage
334
- const db = getDbConfig();
335
- const connectionString = `postgres://${db.user}:${db.password}@${db.host}:${db.port}/${db.database}`;
336
- ```
432
+ ## Custom ConfigSpec
337
433
 
338
- ### API Service Configuration
434
+ Implement the `ConfigSpec` interface to create your own config types:
339
435
 
340
436
  ```typescript
341
- import { defineConfig, env, envNumber, envBoolean, envJsonOptional } from 'ezcfg';
342
-
343
- export const getConfig = defineConfig({
344
- // Server
345
- port: envNumber('PORT'),
346
- host: envOptional('HOST', '0.0.0.0'),
347
-
348
- // API Keys
349
- apiKey: env('API_KEY'),
350
- secretKey: env('SECRET_KEY'),
351
-
352
- // Features
353
- debug: envBoolean('DEBUG', false),
354
- enableMetrics: envBoolean('ENABLE_METRICS', true),
437
+ import type { ConfigSpec } from 'ezcfg';
355
438
 
356
- // CORS
357
- allowedOrigins: envJsonOptional<string[]>('ALLOWED_ORIGINS', ['http://localhost:3000']),
358
- }, { loadEnv: true });
359
- ```
439
+ class RedisConfigSpec implements ConfigSpec<RedisConfig> {
440
+ readonly _type = 'redis';
360
441
 
361
- ### Multi-environment Setup
442
+ constructor(private readonly prefix: string) {}
362
443
 
363
- ```
364
- project/
365
- ├── .env # Shared defaults
366
- ├── .env.local # Local secrets (gitignored)
367
- ├── .env.development # Development settings
368
- ├── .env.production # Production settings
369
- └── .env.test # Test settings
370
- ```
444
+ resolve(errors: string[], envSource?: Record<string, string>): RedisConfig | undefined {
445
+ const source = envSource ?? process.env;
446
+ const url = source[`${this.prefix}_URL`];
371
447
 
372
- ```bash
373
- # .env
374
- LOG_LEVEL=info
375
- API_TIMEOUT=5000
448
+ if (!url) {
449
+ errors.push(`Missing ${this.prefix}_URL`);
450
+ return undefined;
451
+ }
376
452
 
377
- # .env.development
378
- DEBUG=true
379
- API_URL=http://localhost:3000
380
-
381
- # .env.production
382
- DEBUG=false
383
- API_URL=https://api.example.com
384
-
385
- # .env.local (gitignored)
386
- API_KEY=your-secret-key
453
+ return new RedisConfig(url);
454
+ }
455
+ }
387
456
  ```
388
457
 
389
458
  ## License
@@ -0,0 +1,20 @@
1
+ interface ConfigSpec<T> {
2
+ readonly _type: string;
3
+ resolve(errors: string[], envSource?: Record<string, string>): T | undefined;
4
+ }
5
+ type InferSpecType<S> = S extends ConfigSpec<infer T> ? T : S;
6
+ type InferConfigType<S extends Record<string, unknown>> = {
7
+ readonly [K in keyof S]: InferSpecType<S[K]>;
8
+ };
9
+ declare function isConfigSpec(value: unknown): value is ConfigSpec<unknown>;
10
+
11
+ interface DatabaseConfig {
12
+ readonly host: string;
13
+ readonly port: number;
14
+ readonly database: string;
15
+ readonly user: string;
16
+ readonly password?: string;
17
+ toString(): string;
18
+ }
19
+
20
+ export { type ConfigSpec as C, type DatabaseConfig as D, type InferConfigType as I, type InferSpecType as a, isConfigSpec as i };
@@ -0,0 +1,20 @@
1
+ interface ConfigSpec<T> {
2
+ readonly _type: string;
3
+ resolve(errors: string[], envSource?: Record<string, string>): T | undefined;
4
+ }
5
+ type InferSpecType<S> = S extends ConfigSpec<infer T> ? T : S;
6
+ type InferConfigType<S extends Record<string, unknown>> = {
7
+ readonly [K in keyof S]: InferSpecType<S[K]>;
8
+ };
9
+ declare function isConfigSpec(value: unknown): value is ConfigSpec<unknown>;
10
+
11
+ interface DatabaseConfig {
12
+ readonly host: string;
13
+ readonly port: number;
14
+ readonly database: string;
15
+ readonly user: string;
16
+ readonly password?: string;
17
+ toString(): string;
18
+ }
19
+
20
+ export { type ConfigSpec as C, type DatabaseConfig as D, type InferConfigType as I, type InferSpecType as a, isConfigSpec as i };
@@ -0,0 +1,47 @@
1
+ import { I as InferConfigType, C as ConfigSpec } from './database-config-YXQXZ9gp.mjs';
2
+ export { D as DatabaseConfig, a as InferSpecType, i as isConfigSpec } from './database-config-YXQXZ9gp.mjs';
3
+
4
+ declare class ConfigValidationError extends Error {
5
+ readonly errors: string[];
6
+ constructor(errors: string[]);
7
+ }
8
+
9
+ interface ConfigOptions {
10
+ loadEnv?: boolean;
11
+ envLoader?: () => void;
12
+ fromEnvFile?: string;
13
+ }
14
+ declare function defineConfig<S extends Record<string, unknown>>(schema: S, options?: ConfigOptions): () => InferConfigType<S>;
15
+ declare function parseEnvFile(filePath: string): Record<string, string>;
16
+
17
+ declare function loadEnvFiles({ basePath, nodeEnv, }?: {
18
+ basePath?: string;
19
+ nodeEnv?: string;
20
+ }): void;
21
+
22
+ declare class EnvSpec<T> implements ConfigSpec<T> {
23
+ private readonly envKey;
24
+ private readonly required;
25
+ private readonly defaultValue?;
26
+ private readonly transform?;
27
+ readonly _type = "env";
28
+ constructor(envKey: string, required: boolean, defaultValue?: T | undefined, transform?: ((value: string) => T) | undefined);
29
+ resolve(errors: string[], envSource?: Record<string, string>): T | undefined;
30
+ }
31
+ declare function env(key: string): EnvSpec<string>;
32
+ declare function envOptional(key: string, defaultValue?: string): EnvSpec<string | undefined>;
33
+ declare function envNumber(key: string): EnvSpec<number>;
34
+ declare function envNumberOptional(key: string, defaultValue?: number): EnvSpec<number | undefined>;
35
+ declare function envBoolean(key: string, defaultValue?: boolean): EnvSpec<boolean>;
36
+ declare function envJson<T>(key: string): EnvSpec<T>;
37
+ declare function envJsonOptional<T>(key: string, defaultValue?: T): EnvSpec<T | undefined>;
38
+
39
+ declare class ComputedSpec<T> implements ConfigSpec<T> {
40
+ private readonly factory;
41
+ readonly _type = "computed";
42
+ constructor(factory: () => T);
43
+ resolve(errors: string[], _envSource?: Record<string, string>): T | undefined;
44
+ }
45
+ declare function computed<T>(factory: () => T): ComputedSpec<T>;
46
+
47
+ export { ComputedSpec, type ConfigOptions, ConfigSpec, ConfigValidationError, EnvSpec, InferConfigType, computed, defineConfig, env, envBoolean, envJson, envJsonOptional, envNumber, envNumberOptional, envOptional, loadEnvFiles, parseEnvFile };
@@ -0,0 +1,47 @@
1
+ import { I as InferConfigType, C as ConfigSpec } from './database-config-YXQXZ9gp.js';
2
+ export { D as DatabaseConfig, a as InferSpecType, i as isConfigSpec } from './database-config-YXQXZ9gp.js';
3
+
4
+ declare class ConfigValidationError extends Error {
5
+ readonly errors: string[];
6
+ constructor(errors: string[]);
7
+ }
8
+
9
+ interface ConfigOptions {
10
+ loadEnv?: boolean;
11
+ envLoader?: () => void;
12
+ fromEnvFile?: string;
13
+ }
14
+ declare function defineConfig<S extends Record<string, unknown>>(schema: S, options?: ConfigOptions): () => InferConfigType<S>;
15
+ declare function parseEnvFile(filePath: string): Record<string, string>;
16
+
17
+ declare function loadEnvFiles({ basePath, nodeEnv, }?: {
18
+ basePath?: string;
19
+ nodeEnv?: string;
20
+ }): void;
21
+
22
+ declare class EnvSpec<T> implements ConfigSpec<T> {
23
+ private readonly envKey;
24
+ private readonly required;
25
+ private readonly defaultValue?;
26
+ private readonly transform?;
27
+ readonly _type = "env";
28
+ constructor(envKey: string, required: boolean, defaultValue?: T | undefined, transform?: ((value: string) => T) | undefined);
29
+ resolve(errors: string[], envSource?: Record<string, string>): T | undefined;
30
+ }
31
+ declare function env(key: string): EnvSpec<string>;
32
+ declare function envOptional(key: string, defaultValue?: string): EnvSpec<string | undefined>;
33
+ declare function envNumber(key: string): EnvSpec<number>;
34
+ declare function envNumberOptional(key: string, defaultValue?: number): EnvSpec<number | undefined>;
35
+ declare function envBoolean(key: string, defaultValue?: boolean): EnvSpec<boolean>;
36
+ declare function envJson<T>(key: string): EnvSpec<T>;
37
+ declare function envJsonOptional<T>(key: string, defaultValue?: T): EnvSpec<T | undefined>;
38
+
39
+ declare class ComputedSpec<T> implements ConfigSpec<T> {
40
+ private readonly factory;
41
+ readonly _type = "computed";
42
+ constructor(factory: () => T);
43
+ resolve(errors: string[], _envSource?: Record<string, string>): T | undefined;
44
+ }
45
+ declare function computed<T>(factory: () => T): ComputedSpec<T>;
46
+
47
+ export { ComputedSpec, type ConfigOptions, ConfigSpec, ConfigValidationError, EnvSpec, InferConfigType, computed, defineConfig, env, envBoolean, envJson, envJsonOptional, envNumber, envNumberOptional, envOptional, loadEnvFiles, parseEnvFile };
package/dist/index.js ADDED
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ComputedSpec: () => ComputedSpec,
34
+ ConfigValidationError: () => ConfigValidationError,
35
+ EnvSpec: () => EnvSpec,
36
+ computed: () => computed,
37
+ defineConfig: () => defineConfig,
38
+ env: () => env,
39
+ envBoolean: () => envBoolean,
40
+ envJson: () => envJson,
41
+ envJsonOptional: () => envJsonOptional,
42
+ envNumber: () => envNumber,
43
+ envNumberOptional: () => envNumberOptional,
44
+ envOptional: () => envOptional,
45
+ isConfigSpec: () => isConfigSpec,
46
+ loadEnvFiles: () => loadEnvFiles,
47
+ parseEnvFile: () => parseEnvFile
48
+ });
49
+ module.exports = __toCommonJS(index_exports);
50
+
51
+ // src/config-spec.ts
52
+ function isConfigSpec(value) {
53
+ return typeof value === "object" && value !== null && "resolve" in value && typeof value.resolve === "function";
54
+ }
55
+
56
+ // src/errors.ts
57
+ var ConfigValidationError = class extends Error {
58
+ constructor(errors) {
59
+ super(`Config validation failed:
60
+ - ${errors.join("\n - ")}`);
61
+ this.errors = errors;
62
+ this.name = "ConfigValidationError";
63
+ }
64
+ };
65
+
66
+ // src/define-config.ts
67
+ var import_node_fs2 = __toESM(require("fs"));
68
+ var import_dotenv2 = __toESM(require("dotenv"));
69
+
70
+ // src/load-env-files.ts
71
+ var import_node_fs = __toESM(require("fs"));
72
+ var import_node_path = __toESM(require("path"));
73
+ var import_dotenv = __toESM(require("dotenv"));
74
+ function loadEnvFiles({
75
+ basePath,
76
+ nodeEnv
77
+ } = {}) {
78
+ const basePath_ = basePath ?? process.cwd();
79
+ const nodeEnv_ = nodeEnv ?? process.env.NODE_ENV ?? "development";
80
+ const envFiles = [
81
+ ".env",
82
+ ".env.local",
83
+ `.env.${nodeEnv_}`,
84
+ `.env.${nodeEnv_}.local`
85
+ ];
86
+ const loadEnvFile = (filePath) => {
87
+ if (import_node_fs.default.existsSync(filePath)) {
88
+ import_dotenv.default.config({ path: filePath });
89
+ }
90
+ };
91
+ envFiles.reverse().forEach((file) => {
92
+ const filePath = import_node_path.default.resolve(basePath_, file);
93
+ loadEnvFile(filePath);
94
+ });
95
+ }
96
+
97
+ // src/define-config.ts
98
+ function defineConfig(schema, options) {
99
+ let instance = null;
100
+ return () => {
101
+ if (instance && process.env.NODE_ENV !== "test") {
102
+ return instance;
103
+ }
104
+ let envSource;
105
+ if (options?.fromEnvFile) {
106
+ envSource = parseEnvFile(options.fromEnvFile);
107
+ } else if (options?.loadEnv) {
108
+ if (options.envLoader) {
109
+ options.envLoader();
110
+ } else {
111
+ loadEnvFiles();
112
+ }
113
+ }
114
+ const config = {};
115
+ const errors = [];
116
+ for (const [key, value] of Object.entries(schema)) {
117
+ if (isConfigSpec(value)) {
118
+ config[key] = value.resolve(errors, envSource);
119
+ } else {
120
+ config[key] = value;
121
+ }
122
+ }
123
+ if (errors.length > 0) {
124
+ throw new ConfigValidationError(errors);
125
+ }
126
+ instance = config;
127
+ return instance;
128
+ };
129
+ }
130
+ function parseEnvFile(filePath) {
131
+ const content = import_node_fs2.default.readFileSync(filePath, "utf-8");
132
+ return import_dotenv2.default.parse(content);
133
+ }
134
+
135
+ // src/env-spec.ts
136
+ var EnvSpec = class {
137
+ constructor(envKey, required, defaultValue, transform) {
138
+ this.envKey = envKey;
139
+ this.required = required;
140
+ this.defaultValue = defaultValue;
141
+ this.transform = transform;
142
+ }
143
+ _type = "env";
144
+ resolve(errors, envSource) {
145
+ const rawValue = (envSource ?? process.env)[this.envKey];
146
+ if (!rawValue && this.required) {
147
+ errors.push(`Missing required env: ${this.envKey}`);
148
+ return void 0;
149
+ }
150
+ if (rawValue !== void 0) {
151
+ try {
152
+ return this.transform ? this.transform(rawValue) : rawValue;
153
+ } catch (e) {
154
+ errors.push(`Failed to transform ${this.envKey}: ${e.message}`);
155
+ return void 0;
156
+ }
157
+ }
158
+ return this.defaultValue;
159
+ }
160
+ };
161
+ function env(key) {
162
+ return new EnvSpec(key, true);
163
+ }
164
+ function envOptional(key, defaultValue) {
165
+ return new EnvSpec(key, false, defaultValue);
166
+ }
167
+ function envNumber(key) {
168
+ return new EnvSpec(key, true, void 0, Number);
169
+ }
170
+ function envNumberOptional(key, defaultValue) {
171
+ return new EnvSpec(key, false, defaultValue, (v) => v ? Number(v) : void 0);
172
+ }
173
+ function envBoolean(key, defaultValue = false) {
174
+ return new EnvSpec(key, false, defaultValue, (v) => v === "true" || v === "1");
175
+ }
176
+ function envJson(key) {
177
+ return new EnvSpec(key, true, void 0, JSON.parse);
178
+ }
179
+ function envJsonOptional(key, defaultValue) {
180
+ return new EnvSpec(key, false, defaultValue, (v) => v ? JSON.parse(v) : void 0);
181
+ }
182
+
183
+ // src/computed-spec.ts
184
+ var ComputedSpec = class {
185
+ constructor(factory) {
186
+ this.factory = factory;
187
+ }
188
+ _type = "computed";
189
+ resolve(errors, _envSource) {
190
+ try {
191
+ return this.factory();
192
+ } catch (e) {
193
+ errors.push(`Computed value failed: ${e.message}`);
194
+ return void 0;
195
+ }
196
+ }
197
+ };
198
+ function computed(factory) {
199
+ return new ComputedSpec(factory);
200
+ }
201
+ // Annotate the CommonJS export names for ESM import in node:
202
+ 0 && (module.exports = {
203
+ ComputedSpec,
204
+ ConfigValidationError,
205
+ EnvSpec,
206
+ computed,
207
+ defineConfig,
208
+ env,
209
+ envBoolean,
210
+ envJson,
211
+ envJsonOptional,
212
+ envNumber,
213
+ envNumberOptional,
214
+ envOptional,
215
+ isConfigSpec,
216
+ loadEnvFiles,
217
+ parseEnvFile
218
+ });
219
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/config-spec.ts","../src/errors.ts","../src/define-config.ts","../src/load-env-files.ts","../src/env-spec.ts","../src/computed-spec.ts"],"sourcesContent":["export {\n type ConfigSpec,\n type InferSpecType,\n type InferConfigType,\n isConfigSpec,\n} from \"./config-spec\";\n\nexport { ConfigValidationError } from \"./errors\";\n\nexport { defineConfig, parseEnvFile, type ConfigOptions } from \"./define-config\";\nexport { loadEnvFiles } from \"./load-env-files\";\n\nexport {\n EnvSpec,\n env,\n envOptional,\n envNumber,\n envNumberOptional,\n envBoolean,\n envJson,\n envJsonOptional,\n} from \"./env-spec\";\n\nexport { ComputedSpec, computed } from \"./computed-spec\";\n\nexport type { DatabaseConfig } from \"./database-config\";\n","export interface ConfigSpec<T> {\n readonly _type: string;\n resolve(errors: string[], envSource?: Record<string, string>): T | undefined;\n}\n\nexport type InferSpecType<S> = S extends ConfigSpec<infer T> ? T : S;\n\nexport type InferConfigType<S extends Record<string, unknown>> = {\n readonly [K in keyof S]: InferSpecType<S[K]>;\n};\n\nexport function isConfigSpec(value: unknown): value is ConfigSpec<unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"resolve\" in value &&\n typeof (value as ConfigSpec<unknown>).resolve === \"function\"\n );\n}\n","export class ConfigValidationError extends Error {\n constructor(public readonly errors: string[]) {\n super(`Config validation failed:\\n - ${errors.join(\"\\n - \")}`);\n this.name = \"ConfigValidationError\";\n }\n}\n","import fs from \"node:fs\";\n\nimport dotenv from \"dotenv\";\n\nimport { type InferConfigType, isConfigSpec } from \"./config-spec\";\nimport { ConfigValidationError } from \"./errors\";\nimport { loadEnvFiles } from \"./load-env-files\";\n\nexport interface ConfigOptions {\n loadEnv?: boolean;\n envLoader?: () => void;\n fromEnvFile?: string;\n}\n\nexport function defineConfig<S extends Record<string, unknown>>(\n schema: S,\n options?: ConfigOptions\n): () => InferConfigType<S> {\n let instance: InferConfigType<S> | null = null;\n\n return () => {\n if (instance && process.env.NODE_ENV !== \"test\") {\n return instance;\n }\n\n let envSource: Record<string, string> | undefined;\n\n if (options?.fromEnvFile) {\n envSource = parseEnvFile(options.fromEnvFile);\n } else if (options?.loadEnv) {\n if (options.envLoader) {\n options.envLoader();\n } else {\n loadEnvFiles();\n }\n }\n\n const config = {} as Record<string, unknown>;\n const errors: string[] = [];\n\n for (const [key, value] of Object.entries(schema)) {\n if (isConfigSpec(value)) {\n config[key] = value.resolve(errors, envSource);\n } else {\n config[key] = value;\n }\n }\n\n if (errors.length > 0) {\n throw new ConfigValidationError(errors);\n }\n\n instance = config as InferConfigType<S>;\n return instance;\n };\n}\n\nexport function parseEnvFile(filePath: string): Record<string, string> {\n const content = fs.readFileSync(filePath, \"utf-8\");\n return dotenv.parse(content);\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport dotenv from \"dotenv\";\n\nexport function loadEnvFiles({\n basePath,\n nodeEnv,\n}: {\n basePath?: string;\n nodeEnv?: string;\n} = {}): void {\n const basePath_ = basePath ?? process.cwd();\n const nodeEnv_ = nodeEnv ?? process.env.NODE_ENV ?? \"development\";\n\n const envFiles = [\n \".env\",\n \".env.local\",\n `.env.${nodeEnv_}`,\n `.env.${nodeEnv_}.local`,\n ];\n\n const loadEnvFile = (filePath: string) => {\n if (fs.existsSync(filePath)) {\n dotenv.config({ path: filePath });\n }\n };\n\n envFiles.reverse().forEach((file) => {\n const filePath = path.resolve(basePath_, file);\n loadEnvFile(filePath);\n });\n}\n","import type { ConfigSpec } from \"./config-spec\";\n\nexport class EnvSpec<T> implements ConfigSpec<T> {\n readonly _type = \"env\";\n\n constructor(\n private readonly envKey: string,\n private readonly required: boolean,\n private readonly defaultValue?: T,\n private readonly transform?: (value: string) => T\n ) {}\n\n resolve(errors: string[], envSource?: Record<string, string>): T | undefined {\n const rawValue = (envSource ?? process.env)[this.envKey];\n\n if (!rawValue && this.required) {\n errors.push(`Missing required env: ${this.envKey}`);\n return undefined;\n }\n\n if (rawValue !== undefined) {\n try {\n return this.transform ? this.transform(rawValue) : (rawValue as T);\n } catch (e) {\n errors.push(`Failed to transform ${this.envKey}: ${(e as Error).message}`);\n return undefined;\n }\n }\n\n return this.defaultValue;\n }\n}\n\nexport function env(key: string): EnvSpec<string> {\n return new EnvSpec(key, true);\n}\n\nexport function envOptional(key: string, defaultValue?: string): EnvSpec<string | undefined> {\n return new EnvSpec(key, false, defaultValue);\n}\n\nexport function envNumber(key: string): EnvSpec<number> {\n return new EnvSpec(key, true, undefined, Number);\n}\n\nexport function envNumberOptional(key: string, defaultValue?: number): EnvSpec<number | undefined> {\n return new EnvSpec(key, false, defaultValue, (v) => (v ? Number(v) : undefined));\n}\n\nexport function envBoolean(key: string, defaultValue = false): EnvSpec<boolean> {\n return new EnvSpec(key, false, defaultValue, (v) => v === \"true\" || v === \"1\");\n}\n\nexport function envJson<T>(key: string): EnvSpec<T> {\n return new EnvSpec(key, true, undefined, JSON.parse);\n}\n\nexport function envJsonOptional<T>(key: string, defaultValue?: T): EnvSpec<T | undefined> {\n return new EnvSpec(key, false, defaultValue, (v) => (v ? JSON.parse(v) : undefined));\n}\n","import type { ConfigSpec } from \"./config-spec\";\n\nexport class ComputedSpec<T> implements ConfigSpec<T> {\n readonly _type = \"computed\";\n\n constructor(private readonly factory: () => T) {}\n\n resolve(errors: string[], _envSource?: Record<string, string>): T | undefined {\n try {\n return this.factory();\n } catch (e) {\n errors.push(`Computed value failed: ${(e as Error).message}`);\n return undefined;\n }\n }\n}\n\nexport function computed<T>(factory: () => T): ComputedSpec<T> {\n return new ComputedSpec(factory);\n}\n"],"mappings":"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","names":["import_node_fs","import_dotenv","fs","dotenv","path","fs","dotenv"]}