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 +133 -64
- package/dist/database-config-YXQXZ9gp.d.mts +20 -0
- package/dist/database-config-YXQXZ9gp.d.ts +20 -0
- package/dist/index.d.mts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +219 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +168 -0
- package/dist/index.mjs.map +1 -0
- package/dist/postgres.d.mts +74 -0
- package/dist/postgres.d.ts +74 -0
- package/dist/postgres.js +218 -0
- package/dist/postgres.js.map +1 -0
- package/dist/postgres.mjs +189 -0
- package/dist/postgres.mjs.map +1 -0
- package/package.json +6 -1
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
|
-
- **
|
|
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
|
-
##
|
|
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
|
-
|
|
434
|
+
Implement the `ConfigSpec` interface to create your own config types:
|
|
339
435
|
|
|
340
436
|
```typescript
|
|
341
|
-
import {
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
}, { loadEnv: true });
|
|
359
|
-
```
|
|
439
|
+
class RedisConfigSpec implements ConfigSpec<RedisConfig> {
|
|
440
|
+
readonly _type = 'redis';
|
|
360
441
|
|
|
361
|
-
|
|
442
|
+
constructor(private readonly prefix: string) {}
|
|
362
443
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
448
|
+
if (!url) {
|
|
449
|
+
errors.push(`Missing ${this.prefix}_URL`);
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
376
452
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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"]}
|