@fluojs/config 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.ko.md +101 -0
- package/README.md +106 -0
- package/dist/clone.d.ts +2 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +8 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/load.d.ts +33 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +198 -0
- package/dist/module.d.ts +27 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +39 -0
- package/dist/reload-module.d.ts +34 -0
- package/dist/reload-module.d.ts.map +1 -0
- package/dist/reload-module.js +127 -0
- package/dist/service.d.ts +38 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +81 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fluo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @fluojs/config
|
|
2
|
+
|
|
3
|
+
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
|
+
|
|
5
|
+
fluo 애플리케이션을 위한 설정 로드, 병합, 검증, 타입 안전한 런타임 접근을 제공하는 패키지입니다.
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
- [설치](#설치)
|
|
10
|
+
- [사용 시점](#사용-시점)
|
|
11
|
+
- [빠른 시작](#빠른-시작)
|
|
12
|
+
- [주요 기능](#주요-기능)
|
|
13
|
+
- [공개 API](#공개-api)
|
|
14
|
+
- [관련 패키지](#관련-패키지)
|
|
15
|
+
- [예제 소스](#예제-소스)
|
|
16
|
+
|
|
17
|
+
## 설치
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @fluojs/config
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 사용 시점
|
|
24
|
+
|
|
25
|
+
- `.env`와 환경 변수, 런타임 오버라이드를 하나의 설정 스냅샷으로 합쳐야 할 때
|
|
26
|
+
- 여러 소스의 우선순위를 명확하게 유지한 채 설정을 병합해야 할 때
|
|
27
|
+
- 애플리케이션 시작 전에 설정을 검증해서 잘못된 상태로 부팅되는 일을 막고 싶을 때
|
|
28
|
+
- `ConfigService`를 통해 설정 값을 타입 안전하게 읽고 싶을 때
|
|
29
|
+
|
|
30
|
+
## 빠른 시작
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { ConfigModule } from '@fluojs/config';
|
|
34
|
+
import { Module } from '@fluojs/core';
|
|
35
|
+
|
|
36
|
+
@Module({
|
|
37
|
+
imports: [
|
|
38
|
+
ConfigModule.forRoot({
|
|
39
|
+
envFile: '.env',
|
|
40
|
+
processEnv: {
|
|
41
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
42
|
+
},
|
|
43
|
+
defaults: { PORT: '3000' },
|
|
44
|
+
validate: (config) => {
|
|
45
|
+
if (!config.DATABASE_URL) throw new Error('DATABASE_URL이 필요합니다');
|
|
46
|
+
return config;
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
})
|
|
51
|
+
class AppModule {}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
등록 후에는 `ConfigService`를 주입해서 값을 읽습니다.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { ConfigService } from '@fluojs/config';
|
|
58
|
+
|
|
59
|
+
class MyService {
|
|
60
|
+
constructor(private readonly config: ConfigService) {
|
|
61
|
+
const port = this.config.get('PORT');
|
|
62
|
+
const dbUrl = this.config.getOrThrow('DATABASE_URL');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 주요 기능
|
|
68
|
+
|
|
69
|
+
### 명확한 소스 우선순위
|
|
70
|
+
|
|
71
|
+
설정은 `runtimeOverrides` → `processEnv` 옵션으로 전달한 환경 스냅샷 → env 파일 → `defaults` 순서로 병합됩니다.
|
|
72
|
+
|
|
73
|
+
`@fluojs/config`는 주변 환경 변수를 자동으로 스캔하지 않습니다. 환경 기반 값을 우선순위에 포함하려면 부트스트랩 경계에서 `processEnv` 스냅샷을 명시적으로 전달하세요.
|
|
74
|
+
|
|
75
|
+
### 객체 단위 딥 머지
|
|
76
|
+
|
|
77
|
+
일반 객체는 키 기준으로 깊게 병합되고, 배열과 원시값은 더 높은 우선순위 소스가 전체를 덮어씁니다.
|
|
78
|
+
|
|
79
|
+
### 부트스트랩 전 검증
|
|
80
|
+
|
|
81
|
+
`validate` 함수는 모든 소스가 합쳐진 뒤 실행되며, 에러를 던지면 부트스트랩이 즉시 중단됩니다.
|
|
82
|
+
|
|
83
|
+
## 공개 API
|
|
84
|
+
|
|
85
|
+
| 클래스/헬퍼 | 설명 |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `ConfigModule` | 설정을 전역 또는 지역으로 등록하기 위한 모듈입니다. |
|
|
88
|
+
| `ConfigService` | 설정 값에 타입 안전하게 접근하기 위한 읽기 전용 서비스입니다. 스냅샷 교체는 config reload 경로 내부에만 남습니다. |
|
|
89
|
+
| `loadConfig(options)` | 설정을 수동으로 로드하기 위한 함수형 엔트리 포인트입니다. |
|
|
90
|
+
| `createConfigReloader(options)` | 동적 설정 업데이트를 위한 리로더를 생성합니다. |
|
|
91
|
+
|
|
92
|
+
## 관련 패키지
|
|
93
|
+
|
|
94
|
+
- `@fluojs/runtime`: 부트스트랩 중 `loadConfig()`를 호출합니다.
|
|
95
|
+
- `@fluojs/validation`: `validate` 함수 안에서 스키마 기반 검증을 조합할 수 있습니다.
|
|
96
|
+
|
|
97
|
+
## 예제 소스
|
|
98
|
+
|
|
99
|
+
- `packages/config/src/load.ts`
|
|
100
|
+
- `packages/config/src/service.ts`
|
|
101
|
+
- `packages/config/src/load.test.ts`
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @fluojs/config
|
|
2
|
+
|
|
3
|
+
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
|
+
|
|
5
|
+
Configuration loading, merging, validation, and typed runtime access for fluo applications.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [When to Use](#when-to-use)
|
|
11
|
+
- [Quick Start](#quick-start)
|
|
12
|
+
- [Key Capabilities](#key-capabilities)
|
|
13
|
+
- [Public API](#public-api)
|
|
14
|
+
- [Related Packages](#related-packages)
|
|
15
|
+
- [Example Sources](#example-sources)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @fluojs/config
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## When to Use
|
|
24
|
+
|
|
25
|
+
Use this package when you need to:
|
|
26
|
+
- Load configuration from `.env` files and environment variables.
|
|
27
|
+
- Merge multiple configuration sources with strict precedence rules.
|
|
28
|
+
- Validate your application configuration at startup.
|
|
29
|
+
- Access configuration values through a typed `ConfigService`.
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
The `ConfigModule` handles loading and validating your configuration during bootstrap.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Module } from '@fluojs/core';
|
|
37
|
+
import { ConfigModule } from '@fluojs/config';
|
|
38
|
+
|
|
39
|
+
@Module({
|
|
40
|
+
imports: [
|
|
41
|
+
ConfigModule.forRoot({
|
|
42
|
+
envFile: '.env',
|
|
43
|
+
processEnv: {
|
|
44
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
45
|
+
},
|
|
46
|
+
defaults: { PORT: '3000' },
|
|
47
|
+
validate: (config) => {
|
|
48
|
+
if (!config.DATABASE_URL) throw new Error('DATABASE_URL is required');
|
|
49
|
+
return config;
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
],
|
|
53
|
+
})
|
|
54
|
+
class AppModule {}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Once registered, you can inject `ConfigService` to access your values:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { Inject } from '@fluojs/core';
|
|
61
|
+
import { ConfigService } from '@fluojs/config';
|
|
62
|
+
|
|
63
|
+
class MyService {
|
|
64
|
+
constructor(@Inject(ConfigService) private config: ConfigService) {
|
|
65
|
+
const port = this.config.get('PORT');
|
|
66
|
+
const dbUrl = this.config.getOrThrow('DATABASE_URL');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Key Capabilities
|
|
72
|
+
|
|
73
|
+
### Source Precedence
|
|
74
|
+
Configuration is merged in the following order (highest precedence wins):
|
|
75
|
+
1. **Runtime Overrides**: Values passed explicitly via `runtimeOverrides`.
|
|
76
|
+
2. **Process Environment Snapshot**: Values passed via the `processEnv` option.
|
|
77
|
+
3. **Environment File**: Values from the `.env` file (or custom path).
|
|
78
|
+
4. **Defaults**: Values provided in the `defaults` option.
|
|
79
|
+
|
|
80
|
+
`@fluojs/config` does not scan ambient environment variables automatically. Pass an explicit `processEnv` snapshot at the bootstrap boundary when process-backed values should participate in precedence.
|
|
81
|
+
|
|
82
|
+
### Deep Merging
|
|
83
|
+
Plain objects are deep-merged by key. Arrays and primitive values from higher-precedence sources completely replace lower-precedence ones.
|
|
84
|
+
|
|
85
|
+
### Validation
|
|
86
|
+
The `validate` function runs after all sources are merged but before the application starts. If it throws, the application bootstrap fails immediately.
|
|
87
|
+
|
|
88
|
+
## Public API
|
|
89
|
+
|
|
90
|
+
| Class/Helper | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `ConfigModule` | Module for registering configuration globally or locally. |
|
|
93
|
+
| `ConfigService` | Read-only service for typed access to configuration values. Snapshot replacement stays inside the config reload path. |
|
|
94
|
+
| `loadConfig(options)` | Functional entry point for loading configuration manually. |
|
|
95
|
+
| `createConfigReloader(options)` | Creates a reloader for dynamic configuration updates. |
|
|
96
|
+
|
|
97
|
+
## Related Packages
|
|
98
|
+
|
|
99
|
+
- **`@fluojs/runtime`**: Calls `loadConfig` internally during application bootstrap.
|
|
100
|
+
- **`@fluojs/validation`**: Can be used within the `validate` function for schema-based validation.
|
|
101
|
+
|
|
102
|
+
## Example Sources
|
|
103
|
+
|
|
104
|
+
- `packages/config/src/load.ts`
|
|
105
|
+
- `packages/config/src/service.ts`
|
|
106
|
+
- `packages/config/src/load.test.ts`
|
package/dist/clone.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../src/clone.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAMpD"}
|
package/dist/clone.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
package/dist/load.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ConfigDictionary, ConfigLoadOptions, ConfigReloader } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a stateful config reloader that mirrors `loadConfig(...)` semantics and optionally watches the env file.
|
|
4
|
+
*
|
|
5
|
+
* @param options Configuration loading options, including optional watch mode and validation hooks.
|
|
6
|
+
* @returns A reloader that exposes the current snapshot, manual reload, subscriptions, and cleanup.
|
|
7
|
+
* @throws {FluoError} When the initial config load or validation fails.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const reloader = createConfigReloader({ envFile: '.env', watch: true });
|
|
12
|
+
*
|
|
13
|
+
* const subscription = reloader.subscribe((snapshot) => {
|
|
14
|
+
* console.log(snapshot.PORT);
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* reloader.reload();
|
|
18
|
+
* subscription.unsubscribe();
|
|
19
|
+
* reloader.close();
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function createConfigReloader(options: ConfigLoadOptions): ConfigReloader;
|
|
23
|
+
/**
|
|
24
|
+
* Loads, merges, and validates one configuration snapshot without creating long-lived watcher state.
|
|
25
|
+
*
|
|
26
|
+
* Merge precedence stays aligned with the package README contract: `defaults` < env file < `processEnv` < `runtimeOverrides`.
|
|
27
|
+
*
|
|
28
|
+
* @param options Configuration loading options for source precedence, parsing, and validation.
|
|
29
|
+
* @returns A detached normalized configuration dictionary for the current load.
|
|
30
|
+
* @throws {FluoError} When validation throws or the config cannot be normalized.
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadConfig(options: ConfigLoadOptions): ConfigDictionary;
|
|
33
|
+
//# sourceMappingURL=load.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,cAAc,EAIf,MAAM,YAAY,CAAC;AA0MpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CA4B/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAEvE"}
|
package/dist/load.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { FluoError } from '@fluojs/core';
|
|
4
|
+
import { parse as dotenvParse } from 'dotenv';
|
|
5
|
+
import { expand as dotenvExpand } from 'dotenv-expand';
|
|
6
|
+
import { cloneConfigDictionary } from './clone.js';
|
|
7
|
+
function parseEnvContent(content, safeProcessEnv, customParser) {
|
|
8
|
+
if (customParser) {
|
|
9
|
+
return customParser(content);
|
|
10
|
+
}
|
|
11
|
+
const parsed = dotenvParse(content);
|
|
12
|
+
const result = dotenvExpand({
|
|
13
|
+
parsed,
|
|
14
|
+
processEnv: {
|
|
15
|
+
...safeProcessEnv
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return result.parsed ?? {};
|
|
19
|
+
}
|
|
20
|
+
function sanitizeProcessEnv(processEnv) {
|
|
21
|
+
return Object.fromEntries(Object.entries(processEnv).filter(entry => entry[1] !== undefined));
|
|
22
|
+
}
|
|
23
|
+
function normalizeLoadOptions(options) {
|
|
24
|
+
const cwd = options.cwd ?? process.cwd();
|
|
25
|
+
const envFile = options.envFilePath ?? options.envFile ?? join(cwd, '.env');
|
|
26
|
+
const defaults = options.defaults ?? {};
|
|
27
|
+
const processEnv = options.processEnv ?? {};
|
|
28
|
+
const safeProcessEnv = sanitizeProcessEnv(processEnv);
|
|
29
|
+
const runtimeOverrides = options.runtimeOverrides ?? {};
|
|
30
|
+
return {
|
|
31
|
+
defaults,
|
|
32
|
+
envFile,
|
|
33
|
+
parse: options.parse,
|
|
34
|
+
runtimeOverrides,
|
|
35
|
+
safeProcessEnv,
|
|
36
|
+
validate: options.validate
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function readEnvFileValues(options) {
|
|
40
|
+
if (!existsSync(options.envFile)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
return parseEnvContent(readFileSync(options.envFile, 'utf8'), options.safeProcessEnv, options.parse);
|
|
44
|
+
}
|
|
45
|
+
function isPlainObject(value) {
|
|
46
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
function mergeConfigEntries(target, source) {
|
|
49
|
+
const merged = {
|
|
50
|
+
...target
|
|
51
|
+
};
|
|
52
|
+
for (const [key, sourceValue] of Object.entries(source)) {
|
|
53
|
+
const targetValue = merged[key];
|
|
54
|
+
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
|
|
55
|
+
merged[key] = mergeConfigEntries(targetValue, sourceValue);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
merged[key] = cloneConfigDictionary(sourceValue);
|
|
59
|
+
}
|
|
60
|
+
return merged;
|
|
61
|
+
}
|
|
62
|
+
function mergeConfigSources(...sources) {
|
|
63
|
+
let merged = {};
|
|
64
|
+
for (const source of sources) {
|
|
65
|
+
merged = mergeConfigEntries(merged, source);
|
|
66
|
+
}
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
|
69
|
+
function buildMergedConfig(options) {
|
|
70
|
+
const envFileValues = readEnvFileValues(options);
|
|
71
|
+
return mergeConfigSources(options.defaults, envFileValues, options.safeProcessEnv, options.runtimeOverrides);
|
|
72
|
+
}
|
|
73
|
+
function validateConfig(options, merged) {
|
|
74
|
+
try {
|
|
75
|
+
return options.validate ? options.validate(merged) : merged;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new FluoError('Invalid configuration.', {
|
|
78
|
+
code: 'INVALID_CONFIG',
|
|
79
|
+
cause: error
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function resolveConfig(options) {
|
|
84
|
+
return validateConfig(options, buildMergedConfig(options));
|
|
85
|
+
}
|
|
86
|
+
function createSubscription(listeners, listener) {
|
|
87
|
+
listeners.add(listener);
|
|
88
|
+
return {
|
|
89
|
+
unsubscribe() {
|
|
90
|
+
listeners.delete(listener);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function notifyReloadListeners(listeners, snapshot, reason) {
|
|
95
|
+
for (const listener of listeners) {
|
|
96
|
+
listener(cloneConfigDictionary(snapshot), reason);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function notifyReloadErrorListeners(listeners, error, reason) {
|
|
100
|
+
for (const listener of listeners) {
|
|
101
|
+
listener(error, reason);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function applyReload(normalized, state, listeners, reason) {
|
|
105
|
+
const previous = state.current;
|
|
106
|
+
const next = resolveConfig(normalized);
|
|
107
|
+
state.current = next;
|
|
108
|
+
try {
|
|
109
|
+
notifyReloadListeners(listeners, next, reason);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
state.current = previous;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
return cloneConfigDictionary(next);
|
|
115
|
+
}
|
|
116
|
+
function startReloaderWatcher(normalized, options, state, listeners, errorListeners) {
|
|
117
|
+
if (!options.watch || !existsSync(normalized.envFile)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return watch(normalized.envFile, {
|
|
121
|
+
persistent: false
|
|
122
|
+
}, () => {
|
|
123
|
+
try {
|
|
124
|
+
applyReload(normalized, state, listeners, 'watch');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
notifyReloadErrorListeners(errorListeners, error, 'watch');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function closeReloader(state, listeners, errorListeners) {
|
|
131
|
+
if (state.watcher) {
|
|
132
|
+
state.watcher.close();
|
|
133
|
+
state.watcher = undefined;
|
|
134
|
+
}
|
|
135
|
+
listeners.clear();
|
|
136
|
+
errorListeners.clear();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Creates a stateful config reloader that mirrors `loadConfig(...)` semantics and optionally watches the env file.
|
|
141
|
+
*
|
|
142
|
+
* @param options Configuration loading options, including optional watch mode and validation hooks.
|
|
143
|
+
* @returns A reloader that exposes the current snapshot, manual reload, subscriptions, and cleanup.
|
|
144
|
+
* @throws {FluoError} When the initial config load or validation fails.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* const reloader = createConfigReloader({ envFile: '.env', watch: true });
|
|
149
|
+
*
|
|
150
|
+
* const subscription = reloader.subscribe((snapshot) => {
|
|
151
|
+
* console.log(snapshot.PORT);
|
|
152
|
+
* });
|
|
153
|
+
*
|
|
154
|
+
* reloader.reload();
|
|
155
|
+
* subscription.unsubscribe();
|
|
156
|
+
* reloader.close();
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export function createConfigReloader(options) {
|
|
160
|
+
const normalized = normalizeLoadOptions(options);
|
|
161
|
+
const state = {
|
|
162
|
+
current: resolveConfig(normalized),
|
|
163
|
+
watcher: undefined
|
|
164
|
+
};
|
|
165
|
+
const listeners = new Set();
|
|
166
|
+
const errorListeners = new Set();
|
|
167
|
+
state.watcher = startReloaderWatcher(normalized, options, state, listeners, errorListeners);
|
|
168
|
+
return {
|
|
169
|
+
close() {
|
|
170
|
+
closeReloader(state, listeners, errorListeners);
|
|
171
|
+
},
|
|
172
|
+
current() {
|
|
173
|
+
return cloneConfigDictionary(state.current);
|
|
174
|
+
},
|
|
175
|
+
reload() {
|
|
176
|
+
return applyReload(normalized, state, listeners, 'manual');
|
|
177
|
+
},
|
|
178
|
+
subscribe(listener) {
|
|
179
|
+
return createSubscription(listeners, listener);
|
|
180
|
+
},
|
|
181
|
+
subscribeError(listener) {
|
|
182
|
+
return createSubscription(errorListeners, listener);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Loads, merges, and validates one configuration snapshot without creating long-lived watcher state.
|
|
189
|
+
*
|
|
190
|
+
* Merge precedence stays aligned with the package README contract: `defaults` < env file < `processEnv` < `runtimeOverrides`.
|
|
191
|
+
*
|
|
192
|
+
* @param options Configuration loading options for source precedence, parsing, and validation.
|
|
193
|
+
* @returns A detached normalized configuration dictionary for the current load.
|
|
194
|
+
* @throws {FluoError} When validation throws or the config cannot be normalized.
|
|
195
|
+
*/
|
|
196
|
+
export function loadConfig(options) {
|
|
197
|
+
return cloneConfigDictionary(resolveConfig(normalizeLoadOptions(options)));
|
|
198
|
+
}
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ConfigModuleOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Module facade that wires normalized configuration into the application container.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ConfigModule {
|
|
6
|
+
/**
|
|
7
|
+
* Creates a module class that registers `ConfigService` with one normalized configuration snapshot.
|
|
8
|
+
*
|
|
9
|
+
* @param options Configuration module options for env-file loading, validation, precedence, and scope.
|
|
10
|
+
* @returns A module type that can be listed in `imports` during bootstrap.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* @Module({
|
|
15
|
+
* imports: [
|
|
16
|
+
* ConfigModule.forRoot({
|
|
17
|
+
* envFile: '.env',
|
|
18
|
+
* defaults: { PORT: '3000' },
|
|
19
|
+
* }),
|
|
20
|
+
* ],
|
|
21
|
+
* })
|
|
22
|
+
* class AppModule {}
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
static forRoot(options?: ConfigModuleOptions): new () => ConfigModule;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD;;GAEG;AACH,qBAAa,YAAY;IACvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,UAAU,YAAY;CAgBtE"}
|
package/dist/module.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineModuleMetadata } from '@fluojs/core/internal';
|
|
2
|
+
import { loadConfig } from './load.js';
|
|
3
|
+
import { ConfigService } from './service.js';
|
|
4
|
+
/**
|
|
5
|
+
* Module facade that wires normalized configuration into the application container.
|
|
6
|
+
*/
|
|
7
|
+
export class ConfigModule {
|
|
8
|
+
/**
|
|
9
|
+
* Creates a module class that registers `ConfigService` with one normalized configuration snapshot.
|
|
10
|
+
*
|
|
11
|
+
* @param options Configuration module options for env-file loading, validation, precedence, and scope.
|
|
12
|
+
* @returns A module type that can be listed in `imports` during bootstrap.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* @Module({
|
|
17
|
+
* imports: [
|
|
18
|
+
* ConfigModule.forRoot({
|
|
19
|
+
* envFile: '.env',
|
|
20
|
+
* defaults: { PORT: '3000' },
|
|
21
|
+
* }),
|
|
22
|
+
* ],
|
|
23
|
+
* })
|
|
24
|
+
* class AppModule {}
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
static forRoot(options) {
|
|
28
|
+
class ConfigModuleImpl extends ConfigModule {}
|
|
29
|
+
defineModuleMetadata(ConfigModuleImpl, {
|
|
30
|
+
global: options?.isGlobal ?? true,
|
|
31
|
+
exports: [ConfigService],
|
|
32
|
+
providers: [{
|
|
33
|
+
provide: ConfigService,
|
|
34
|
+
useFactory: () => new ConfigService(loadConfig(options ?? {}))
|
|
35
|
+
}]
|
|
36
|
+
});
|
|
37
|
+
return ConfigModuleImpl;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ConfigService } from './service.js';
|
|
2
|
+
import type { ConfigDictionary, ConfigLoadOptions, ConfigReloadErrorListener, ConfigReloader, ConfigReloadListener, ConfigReloadSubscription } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Exposes the config reload manager contract for dependency injection.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONFIG_RELOADER: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* Lazily creates and coordinates the active config reloader instance.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ConfigReloadManager implements ConfigReloader {
|
|
11
|
+
private readonly config;
|
|
12
|
+
private readonly options;
|
|
13
|
+
private reloader;
|
|
14
|
+
private reloadForwarder;
|
|
15
|
+
private errorForwarder;
|
|
16
|
+
private readonly reloadListeners;
|
|
17
|
+
private readonly errorListeners;
|
|
18
|
+
constructor(config: ConfigService, options: ConfigLoadOptions);
|
|
19
|
+
current(): ConfigDictionary;
|
|
20
|
+
reload(): ConfigDictionary;
|
|
21
|
+
subscribe(listener: ConfigReloadListener): ConfigReloadSubscription;
|
|
22
|
+
subscribeError(listener: ConfigReloadErrorListener): ConfigReloadSubscription;
|
|
23
|
+
close(): void;
|
|
24
|
+
onApplicationBootstrap(): void;
|
|
25
|
+
onModuleDestroy(): void;
|
|
26
|
+
private ensureReloader;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Registers config reload services and exports the shared reloader token.
|
|
30
|
+
*/
|
|
31
|
+
export declare class ConfigReloadModule {
|
|
32
|
+
static forRoot(options?: ConfigLoadOptions): new () => ConfigReloadModule;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=reload-module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reload-module.d.ts","sourceRoot":"","sources":["../src/reload-module.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAgC,MAAM,cAAc,CAAC;AAC3E,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,yBAAyB,EACzB,cAAc,EACd,oBAAoB,EACpB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAIpB;;GAEG;AACH,eAAO,MAAM,eAAe,eAAiC,CAAC;AAY9D;;GAEG;AACH,qBACa,mBAAoB,YAAW,cAAc;IAQtD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,eAAe,CAAuC;IAC9D,OAAO,CAAC,cAAc,CAAuC;IAC7D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAmC;IACnE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwC;gBAGpD,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,iBAAiB;IAG7C,OAAO,IAAI,gBAAgB;IAI3B,MAAM,IAAI,gBAAgB;IAI1B,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,wBAAwB;IAInE,cAAc,CAAC,QAAQ,EAAE,yBAAyB,GAAG,wBAAwB;IAI7E,KAAK,IAAI,IAAI;IAeb,sBAAsB,IAAI,IAAI;IAQ9B,eAAe,IAAI,IAAI;IAIvB,OAAO,CAAC,cAAc;CA8BvB;AAED;;GAEG;AACH,qBAAa,kBAAkB;IAC7B,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,UAAU,kBAAkB;CAsB1E"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
let _initClass;
|
|
2
|
+
function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
|
|
3
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
4
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
5
|
+
function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
|
|
6
|
+
function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
|
|
7
|
+
import { Inject } from '@fluojs/core';
|
|
8
|
+
import { defineModuleMetadata } from '@fluojs/core/internal';
|
|
9
|
+
import { cloneConfigDictionary } from './clone.js';
|
|
10
|
+
import { createConfigReloader } from './load.js';
|
|
11
|
+
import { ConfigService, replaceConfigServiceSnapshot } from './service.js';
|
|
12
|
+
const CONFIG_RELOAD_OPTIONS = Symbol('fluo.config.reload-options');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Exposes the config reload manager contract for dependency injection.
|
|
16
|
+
*/
|
|
17
|
+
export const CONFIG_RELOADER = Symbol('fluo.config.reloader');
|
|
18
|
+
function createSubscription(listeners, listener) {
|
|
19
|
+
listeners.add(listener);
|
|
20
|
+
return {
|
|
21
|
+
unsubscribe() {
|
|
22
|
+
listeners.delete(listener);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lazily creates and coordinates the active config reloader instance.
|
|
29
|
+
*/
|
|
30
|
+
let _ConfigReloadManager;
|
|
31
|
+
class ConfigReloadManager {
|
|
32
|
+
static {
|
|
33
|
+
[_ConfigReloadManager, _initClass] = _applyDecs(this, [Inject(ConfigService, CONFIG_RELOAD_OPTIONS)], []).c;
|
|
34
|
+
}
|
|
35
|
+
reloader;
|
|
36
|
+
reloadForwarder;
|
|
37
|
+
errorForwarder;
|
|
38
|
+
reloadListeners = new Set();
|
|
39
|
+
errorListeners = new Set();
|
|
40
|
+
constructor(config, options) {
|
|
41
|
+
this.config = config;
|
|
42
|
+
this.options = options;
|
|
43
|
+
}
|
|
44
|
+
current() {
|
|
45
|
+
return this.ensureReloader().current();
|
|
46
|
+
}
|
|
47
|
+
reload() {
|
|
48
|
+
return this.ensureReloader().reload();
|
|
49
|
+
}
|
|
50
|
+
subscribe(listener) {
|
|
51
|
+
return createSubscription(this.reloadListeners, listener);
|
|
52
|
+
}
|
|
53
|
+
subscribeError(listener) {
|
|
54
|
+
return createSubscription(this.errorListeners, listener);
|
|
55
|
+
}
|
|
56
|
+
close() {
|
|
57
|
+
this.reloadForwarder?.unsubscribe();
|
|
58
|
+
this.reloadForwarder = undefined;
|
|
59
|
+
this.errorForwarder?.unsubscribe();
|
|
60
|
+
this.errorForwarder = undefined;
|
|
61
|
+
if (this.reloader) {
|
|
62
|
+
this.reloader.close();
|
|
63
|
+
this.reloader = undefined;
|
|
64
|
+
}
|
|
65
|
+
this.reloadListeners.clear();
|
|
66
|
+
this.errorListeners.clear();
|
|
67
|
+
}
|
|
68
|
+
onApplicationBootstrap() {
|
|
69
|
+
if (!this.options.watch) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.ensureReloader();
|
|
73
|
+
}
|
|
74
|
+
onModuleDestroy() {
|
|
75
|
+
this.close();
|
|
76
|
+
}
|
|
77
|
+
ensureReloader() {
|
|
78
|
+
if (this.reloader) {
|
|
79
|
+
return this.reloader;
|
|
80
|
+
}
|
|
81
|
+
const reloader = createConfigReloader(this.options);
|
|
82
|
+
this.reloadForwarder = reloader.subscribe((nextConfig, reason) => {
|
|
83
|
+
const previousConfig = this.config.snapshot();
|
|
84
|
+
try {
|
|
85
|
+
replaceConfigServiceSnapshot(this.config, nextConfig);
|
|
86
|
+
for (const listener of this.reloadListeners) {
|
|
87
|
+
listener(cloneConfigDictionary(nextConfig), reason);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
replaceConfigServiceSnapshot(this.config, previousConfig);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
this.errorForwarder = reloader.subscribeError((error, reason) => {
|
|
95
|
+
for (const listener of this.errorListeners) {
|
|
96
|
+
listener(error, reason);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
this.reloader = reloader;
|
|
100
|
+
return reloader;
|
|
101
|
+
}
|
|
102
|
+
static {
|
|
103
|
+
_initClass();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Registers config reload services and exports the shared reloader token.
|
|
109
|
+
*/
|
|
110
|
+
export { _ConfigReloadManager as ConfigReloadManager };
|
|
111
|
+
export class ConfigReloadModule {
|
|
112
|
+
static forRoot(options) {
|
|
113
|
+
const loadOptions = options ?? {};
|
|
114
|
+
class ConfigReloadModuleImpl extends ConfigReloadModule {}
|
|
115
|
+
defineModuleMetadata(ConfigReloadModuleImpl, {
|
|
116
|
+
exports: [CONFIG_RELOADER],
|
|
117
|
+
providers: [{
|
|
118
|
+
provide: CONFIG_RELOAD_OPTIONS,
|
|
119
|
+
useValue: loadOptions
|
|
120
|
+
}, _ConfigReloadManager, {
|
|
121
|
+
provide: CONFIG_RELOADER,
|
|
122
|
+
useExisting: _ConfigReloadManager
|
|
123
|
+
}]
|
|
124
|
+
});
|
|
125
|
+
return ConfigReloadModuleImpl;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ConfigDictionary, DotPaths, DotValue } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Typed read-only facade over the normalized runtime configuration dictionary.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ConfigService<T extends Record<string, unknown> = ConfigDictionary> {
|
|
6
|
+
private values;
|
|
7
|
+
constructor(values: T);
|
|
8
|
+
/**
|
|
9
|
+
* Returns a config value by key (including dot-path keys) or `undefined` when missing.
|
|
10
|
+
*
|
|
11
|
+
* @param key Configuration key or dot-path key to resolve from the current snapshot.
|
|
12
|
+
* @returns The resolved value clone for object-like entries, or `undefined` when the key does not exist.
|
|
13
|
+
*/
|
|
14
|
+
get<K extends DotPaths<T>>(key: K): DotValue<T, K & string> | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Returns a config value by key and throws when the key is missing.
|
|
17
|
+
*
|
|
18
|
+
* @param key Configuration key or dot-path key that must exist in the current snapshot.
|
|
19
|
+
* @returns The resolved value clone for object-like entries.
|
|
20
|
+
* @throws {FluoError} When the key is absent (`code: 'CONFIG_KEY_MISSING'`).
|
|
21
|
+
*/
|
|
22
|
+
getOrThrow<K extends DotPaths<T>>(key: K): DotValue<T, K & string>;
|
|
23
|
+
/**
|
|
24
|
+
* Returns a deep-cloned snapshot of the current normalized config dictionary.
|
|
25
|
+
*
|
|
26
|
+
* @returns A detached deep clone of the current configuration snapshot.
|
|
27
|
+
*/
|
|
28
|
+
snapshot(): ConfigDictionary;
|
|
29
|
+
private _resolve;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Replaces the underlying configuration snapshot of a `ConfigService`.
|
|
33
|
+
*
|
|
34
|
+
* @param service The `ConfigService` instance to update.
|
|
35
|
+
* @param values The new configuration dictionary.
|
|
36
|
+
*/
|
|
37
|
+
export declare function replaceConfigServiceSnapshot<T extends Record<string, unknown>>(service: ConfigService<T>, values: T): void;
|
|
38
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAMvE;;GAEG;AACH,qBAAa,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB;IAC7E,OAAO,CAAC,MAAM,CAAI;gBAEN,MAAM,EAAE,CAAC;IAIrB;;;;;OAKG;IACH,GAAG,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,GAAG,SAAS;IAIvE;;;;;;OAMG;IACH,UAAU,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC;IAUlE;;;;OAIG;IACH,QAAQ,IAAI,gBAAgB;IAI5B,OAAO,CAAC,QAAQ;CAuBjB;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5E,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,MAAM,EAAE,CAAC,GACR,IAAI,CAEN"}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { FluoError } from '@fluojs/core';
|
|
2
|
+
import { cloneConfigDictionary } from './clone.js';
|
|
3
|
+
function hasOwn(value, key) {
|
|
4
|
+
return typeof value === 'object' && value !== null && Object.hasOwn(value, key);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Typed read-only facade over the normalized runtime configuration dictionary.
|
|
9
|
+
*/
|
|
10
|
+
export class ConfigService {
|
|
11
|
+
values;
|
|
12
|
+
constructor(values) {
|
|
13
|
+
this.values = cloneConfigDictionary(values);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns a config value by key (including dot-path keys) or `undefined` when missing.
|
|
18
|
+
*
|
|
19
|
+
* @param key Configuration key or dot-path key to resolve from the current snapshot.
|
|
20
|
+
* @returns The resolved value clone for object-like entries, or `undefined` when the key does not exist.
|
|
21
|
+
*/
|
|
22
|
+
get(key) {
|
|
23
|
+
return this._resolve(key);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns a config value by key and throws when the key is missing.
|
|
28
|
+
*
|
|
29
|
+
* @param key Configuration key or dot-path key that must exist in the current snapshot.
|
|
30
|
+
* @returns The resolved value clone for object-like entries.
|
|
31
|
+
* @throws {FluoError} When the key is absent (`code: 'CONFIG_KEY_MISSING'`).
|
|
32
|
+
*/
|
|
33
|
+
getOrThrow(key) {
|
|
34
|
+
const value = this._resolve(key);
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
throw new FluoError(`Missing config key: ${String(key)}`, {
|
|
37
|
+
code: 'CONFIG_KEY_MISSING'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns a deep-cloned snapshot of the current normalized config dictionary.
|
|
45
|
+
*
|
|
46
|
+
* @returns A detached deep clone of the current configuration snapshot.
|
|
47
|
+
*/
|
|
48
|
+
snapshot() {
|
|
49
|
+
return cloneConfigDictionary(this.values);
|
|
50
|
+
}
|
|
51
|
+
_resolve(key) {
|
|
52
|
+
let resolved;
|
|
53
|
+
if (hasOwn(this.values, key)) {
|
|
54
|
+
resolved = this.values[key];
|
|
55
|
+
} else {
|
|
56
|
+
const parts = key.split('.');
|
|
57
|
+
let current = this.values;
|
|
58
|
+
for (const part of parts) {
|
|
59
|
+
if (!hasOwn(current, part)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
current = current[part];
|
|
63
|
+
}
|
|
64
|
+
resolved = current;
|
|
65
|
+
}
|
|
66
|
+
if (typeof resolved === 'object' && resolved !== null) {
|
|
67
|
+
return cloneConfigDictionary(resolved);
|
|
68
|
+
}
|
|
69
|
+
return resolved;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Replaces the underlying configuration snapshot of a `ConfigService`.
|
|
75
|
+
*
|
|
76
|
+
* @param service The `ConfigService` instance to update.
|
|
77
|
+
* @param values The new configuration dictionary.
|
|
78
|
+
*/
|
|
79
|
+
export function replaceConfigServiceSnapshot(service, values) {
|
|
80
|
+
service['values'] = cloneConfigDictionary(values);
|
|
81
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain JSON-like object used as the normalized configuration snapshot shape.
|
|
3
|
+
*/
|
|
4
|
+
export type ConfigDictionary = Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* Nested dot-path key helper.
|
|
7
|
+
* Produces "a" | "a.b" | "a.b.c" keys from a Record type.
|
|
8
|
+
*/
|
|
9
|
+
export type DotPaths<T, Prefix extends string = ''> = T extends Record<string, unknown> ? {
|
|
10
|
+
[K in keyof T & string]: `${Prefix}${K}` | DotPaths<T[K], `${Prefix}${K}.`>;
|
|
11
|
+
}[keyof T & string] : never;
|
|
12
|
+
/**
|
|
13
|
+
* Resolves a dot-path key to its leaf value type.
|
|
14
|
+
*/
|
|
15
|
+
export type DotValue<T, K extends string> = K extends keyof T ? T[K] : K extends `${infer Head}.${infer Tail}` ? Head extends keyof T ? DotValue<T[Head], Tail> : never : never;
|
|
16
|
+
/**
|
|
17
|
+
* Module-level configuration options for loading and validating application config.
|
|
18
|
+
*/
|
|
19
|
+
export interface ConfigModuleOptions {
|
|
20
|
+
envFile?: string;
|
|
21
|
+
envFilePath?: string;
|
|
22
|
+
processEnv?: NodeJS.ProcessEnv;
|
|
23
|
+
validate?: (raw: ConfigDictionary) => ConfigDictionary;
|
|
24
|
+
defaults?: ConfigDictionary;
|
|
25
|
+
/** Supply a custom file parser (e.g. for YAML or TOML). Receives raw file content,
|
|
26
|
+
* returns a flat key-value record. Defaults to dotenv parsing. */
|
|
27
|
+
parse?: (content: string) => Record<string, string>;
|
|
28
|
+
watch?: boolean;
|
|
29
|
+
isGlobal?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extended load options for one-off config loads and reloaders outside `ConfigModule.forRoot(...)`.
|
|
33
|
+
*/
|
|
34
|
+
export interface ConfigLoadOptions extends ConfigModuleOptions {
|
|
35
|
+
cwd?: string;
|
|
36
|
+
runtimeOverrides?: ConfigDictionary;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Reason attached to config reload notifications.
|
|
40
|
+
*/
|
|
41
|
+
export type ConfigReloadReason = 'manual' | 'watch';
|
|
42
|
+
/**
|
|
43
|
+
* Listener invoked after a config reload succeeds.
|
|
44
|
+
*/
|
|
45
|
+
export type ConfigReloadListener = (snapshot: ConfigDictionary, reason: ConfigReloadReason) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Listener invoked when a watched reload attempt fails.
|
|
48
|
+
*/
|
|
49
|
+
export type ConfigReloadErrorListener = (error: unknown, reason: ConfigReloadReason) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Disposable subscription handle returned from reloader listener registration.
|
|
52
|
+
*/
|
|
53
|
+
export interface ConfigReloadSubscription {
|
|
54
|
+
unsubscribe(): void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Stateful config reloader contract returned by {@link createConfigReloader}.
|
|
58
|
+
*/
|
|
59
|
+
export interface ConfigReloader {
|
|
60
|
+
current(): ConfigDictionary;
|
|
61
|
+
reload(): ConfigDictionary;
|
|
62
|
+
subscribe(listener: ConfigReloadListener): ConfigReloadSubscription;
|
|
63
|
+
subscribeError(listener: ConfigReloadErrorListener): ConfigReloadSubscription;
|
|
64
|
+
close(): void;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEvD;;;GAGG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,EAAE,MAAM,SAAS,MAAM,GAAG,EAAE,IAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnF;KACG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,GAClB,GAAG,MAAM,GAAG,CAAC,EAAE,GACf,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC;CACrC,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,GACnB,KAAK,CAAC;AAEV;;GAEG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,MAAM,CAAC,GACzD,CAAC,CAAC,CAAC,CAAC,GACJ,CAAC,SAAS,GAAG,MAAM,IAAI,IAAI,MAAM,IAAI,EAAE,GACrC,IAAI,SAAS,MAAM,CAAC,GAClB,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,GACvB,KAAK,GACP,KAAK,CAAC;AAEZ;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;IACvD,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;uEACmE;IACnE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAEpG;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAE7F;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,WAAW,IAAI,IAAI,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,gBAAgB,CAAC;IAC5B,MAAM,IAAI,gBAAgB,CAAC;IAC3B,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,wBAAwB,CAAC;IACpE,cAAc,CAAC,QAAQ,EAAE,yBAAyB,GAAG,wBAAwB,CAAC;IAC9E,KAAK,IAAI,IAAI,CAAC;CACf"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluojs/config",
|
|
3
|
+
"description": "Configuration loading, merging, validation, and typed runtime access for Fluo applications.",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"fluo",
|
|
6
|
+
"config",
|
|
7
|
+
"configuration",
|
|
8
|
+
"environment",
|
|
9
|
+
"typed-config"
|
|
10
|
+
],
|
|
11
|
+
"version": "1.0.0-beta.1",
|
|
12
|
+
"private": false,
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/fluojs/fluo.git",
|
|
17
|
+
"directory": "packages/config"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20.0.0"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"dotenv": "^16.0.0",
|
|
39
|
+
"dotenv-expand": "^11.0.0",
|
|
40
|
+
"@fluojs/core": "^1.0.0-beta.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"vitest": "^3.2.4"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|
|
47
|
+
"build": "pnpm exec babel src --extensions .ts --ignore 'src/**/*.test.ts' --out-dir dist --config-file ../../tooling/babel/babel.config.cjs && pnpm exec tsc -p tsconfig.build.json",
|
|
48
|
+
"typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
|
|
49
|
+
"test": "pnpm exec vitest run -c vitest.config.ts",
|
|
50
|
+
"test:watch": "pnpm exec vitest -c vitest.config.ts"
|
|
51
|
+
}
|
|
52
|
+
}
|