@fluojs/config 1.0.4 → 2.0.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.ko.md +53 -8
- package/README.md +53 -8
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +139 -47
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +4 -7
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +5 -0
- package/dist/reload-module.d.ts +9 -0
- package/dist/reload-module.d.ts.map +1 -1
- package/dist/reload-module.js +19 -2
- package/dist/types.d.ts +18 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -7
package/README.ko.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
fluo 애플리케이션을 위한 설정 로드, 병합, 검증, 타입 안전한 런타임 접근을 제공하는 패키지입니다.
|
|
6
6
|
|
|
7
|
+
Coordinated Node 24 릴리스를 준비한다면 패키지 업그레이드 전에 [소비자 마이그레이션 가이드](../../docs/getting-started/migrate-node24.ko.md)를 따르세요.
|
|
8
|
+
|
|
7
9
|
## 목차
|
|
8
10
|
|
|
9
11
|
- [설치](#설치)
|
|
@@ -20,7 +22,7 @@ fluo 애플리케이션을 위한 설정 로드, 병합, 검증, 타입 안전
|
|
|
20
22
|
npm install @fluojs/config
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
패키지는
|
|
25
|
+
패키지는 의도적으로 package-wide `engines.node`를 선언하지 않습니다. `ConfigService`, merge/validation/clone 동작, `loadConfig({ defaults, processEnv, runtimeOverrides })`는 portable하며 `process.cwd()`, 기본 `.env` path, Node filesystem/path/crypto builtin을 해석하지 않습니다. Env-file loading, 기본 `.env` loading, watch mode는 Node 전용 기능입니다. 이 경로는 지원 범위 Node.js `>=24.0.0 <27`의 `process.getBuiltinModule(...)`을 통해 builtin을 lazy하게 해석하며 host가 해당 경계를 제공하지 않으면 in-memory option을 사용하거나 Node.js에서 실행하라는 guidance와 함께 `CONFIG_RUNTIME_UNAVAILABLE`을 던집니다. 이 guard는 feature capability를 확인하며 Node version 비교나 portable root import 차단을 추가하지 않습니다.
|
|
24
26
|
|
|
25
27
|
## 사용 시점
|
|
26
28
|
|
|
@@ -56,7 +58,7 @@ const EnvSchema = z.object({
|
|
|
56
58
|
class AppModule {}
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
env file이 절대 경로나 미리 해석된 경로에 있다면 `envFilePath`를 사용하고, 기본 dotenv parser 대신 다른 flat-file parser가 필요하다면 `parse`를 전달하세요.
|
|
61
|
+
env file이 절대 경로나 미리 해석된 경로에 있다면 `envFilePath`를 사용하고, 여러 env file을 명시적인 순서로 계층화해야 한다면 `envFilePaths`를 사용하며, 기본 dotenv parser 대신 다른 flat-file parser가 필요하다면 `parse`를 전달하세요.
|
|
60
62
|
|
|
61
63
|
등록 후에는 `ConfigService`를 주입해서 값을 읽습니다.
|
|
62
64
|
|
|
@@ -75,6 +77,19 @@ class MyService {
|
|
|
75
77
|
|
|
76
78
|
## 주요 기능
|
|
77
79
|
|
|
80
|
+
### NestJS 등록 마이그레이션
|
|
81
|
+
|
|
82
|
+
`ConfigModule`은 동기 `forRoot(...)` registration만 노출하며 `ConfigModule.forRootAsync(...)`에 대응하는 API는 없습니다. `@nestjs/config`에서 마이그레이션할 때는 다음 경계를 따르세요.
|
|
83
|
+
|
|
84
|
+
- Remote secret과 다른 비동기 source는 module graph를 정의하기 전에 application-owned bootstrap boundary에서 resolve한 뒤, 최종 값을 동기 registration call에 전달합니다.
|
|
85
|
+
- NestJS `load` factory는 `defaults` 또는 `runtimeOverrides`의 nested plain object로 옮깁니다. Deep merge와 dot-path `ConfigService` 접근을 위해 nesting을 그대로 유지하세요.
|
|
86
|
+
- `@fluojs/config`는 ambient environment variable을 scan하지 않으므로 명시적 `processEnv` snapshot을 전달합니다.
|
|
87
|
+
- NestJS `validate` callback은 `schema`에 전달하는 동기 Standard Schema로 바꿉니다. 비동기 schema 결과는 거부됩니다.
|
|
88
|
+
- NestJS `isGlobal`이 아니라 `global`을 사용합니다. Visibility는 기본적으로 global이며, `global: false`로 module-local visibility를 선택합니다.
|
|
89
|
+
- Call site는 single-key 형태로 재작성합니다. `ConfigService.get(key)`와 `getOrThrow(key)`는 key 하나만 받으며 NestJS default-value 또는 options overload를 노출하지 않습니다. 기본값은 `defaults` 또는 `schema` output이 소유하거나, `get(key)` 결과에 명시적인 `??` fallback을 적용합니다.
|
|
90
|
+
|
|
91
|
+
공유 validated snapshot bootstrap pattern과 HTTP adapter boundary는 canonical [NestJS configuration migration guide](../../docs/getting-started/migrate-from-nestjs.ko.md)를 참고하세요.
|
|
92
|
+
|
|
78
93
|
### 명확한 소스 우선순위
|
|
79
94
|
|
|
80
95
|
설정은 `runtimeOverrides` → `processEnv` 옵션으로 전달한 환경 스냅샷 → env 파일 → `defaults` 순서로 병합됩니다.
|
|
@@ -83,7 +98,34 @@ class MyService {
|
|
|
83
98
|
|
|
84
99
|
`envFilePath`는 `envFile`보다 우선하며, `parse`를 사용하면 flat key/value 파일을 위한 custom parser로 dotenv parsing을 대체할 수 있습니다. 빈 load/module option은 `loadConfig({})`와 `ConfigModule.forRoot()`에 대해 기본 `<cwd>/.env` 동작을 보존합니다. 누락된 env file은 load 시 빈 입력처럼 처리됩니다. watch mode에서는 parent directory도 관찰하므로 나중에 파일을 생성해도 reload를 트리거할 수 있습니다.
|
|
85
100
|
|
|
86
|
-
|
|
101
|
+
### 순서가 있는 다중 env file loading
|
|
102
|
+
|
|
103
|
+
`envFilePaths`는 명시적으로 순서가 정해진 env file 목록 하나를 받습니다. 목록은 낮은 우선순위에서 높은 우선순위로 병합되어 단일 env-file tier를 구성하므로, 여전히 `defaults`보다 위에 있고 `processEnv`와 `runtimeOverrides`보다 아래에 있습니다.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
ConfigModule.forRoot({
|
|
107
|
+
envFilePaths: ['.env', '.env.production', '.env.production.local'],
|
|
108
|
+
processEnv: {
|
|
109
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
110
|
+
},
|
|
111
|
+
schema: EnvSchema,
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
contract는 다음과 같습니다.
|
|
116
|
+
|
|
117
|
+
- 뒤쪽 entry가 앞쪽 entry를 이깁니다. plain object는 여전히 deep merge되고 array는 여전히 교체됩니다.
|
|
118
|
+
- 상대 경로 entry는 `cwd`(기본값 `process.cwd()`) 기준으로 해석되고, 절대 경로 entry는 그대로 사용됩니다.
|
|
119
|
+
- 누락된 파일은 아무 값도 기여하지 않으며 load를 실패시키지 않습니다.
|
|
120
|
+
- `envFilePaths: []`는 기본 `<cwd>/.env` fallback을 포함해 env-file loading 자체를 명시적으로 해제합니다.
|
|
121
|
+
- `envFilePaths`를 `envFile` 또는 `envFilePath`와 함께 쓰면 `INVALID_CONFIG`로 실패하며, 해석 결과가 중복된 경로나 빈 entry도 동일하게 실패합니다.
|
|
122
|
+
- schema는 개별 파일이 아니라 완전히 병합된 결과를 한 번만 검증합니다.
|
|
123
|
+
- watch mode에서는 서로 다른 parent directory마다 watcher를 하나씩만 시작하고, 목록에 포함된 파일이 변경되면 전체 목록을 다시 계산하며, 우선순위가 높은 파일을 삭제하면 남은 파일로 fallback합니다. 검증 실패 시에는 마지막 유효 snapshot을 유지합니다.
|
|
124
|
+
- 자동 profile 탐색은 패키지 밖에 남습니다. 정확한 목록과 순서는 caller가 결정합니다.
|
|
125
|
+
|
|
126
|
+
패키지는 `NODE_ENV`에서 env file 이름을 유도하지 않습니다. 환경별 계층화가 필요하다면 bootstrap boundary에서 목록을 직접 구성하세요.
|
|
127
|
+
|
|
128
|
+
Root `@fluojs/config` 패키지를 import하는 것만으로는 Node filesystem, path, crypto builtin을 해석하지 않습니다. `ConfigService`, option type, 또는 명시적 in-memory 입력을 쓰는 `loadConfig(...)` consumer는 non-Node runtime에서도 지원됩니다. Env-file, 기본 `.env`, watch 실행은 Node 전용이며 `process.getBuiltinModule(...)`을 제공하는 host가 필요합니다. 지원하지 않는 host에서는 eager import failure 대신 문서화된 `CONFIG_RUNTIME_UNAVAILABLE` error가 발생합니다.
|
|
87
129
|
|
|
88
130
|
### 객체 단위 딥 머지
|
|
89
131
|
|
|
@@ -101,9 +143,9 @@ Root `@fluojs/config` 패키지를 import하는 것만으로는 Node filesystem,
|
|
|
101
143
|
|
|
102
144
|
`ConfigReloadManager.reload()`는 리로드 작업을 직렬화합니다. 현재 리로드가 listener 알림을 수행하는 동안 다른 리로드가 요청되면 후속 리로드는 큐에 들어가 활성 알림이 끝난 뒤 적용됩니다. 활성 알림이 실패하면 직전 snapshot을 복구하고 큐에 있던 리로드는 폐기합니다. 동일한 직렬화와 rollback 계약은 `createConfigReloader(...).reload()`에도 적용되며, watch로 시작된 알림 중 큐에 들어간 manual reload도 이 계약을 따릅니다.
|
|
103
145
|
|
|
104
|
-
Module registration과 reloader 생성은 `schema`로 전달한 nested Standard Schema validator object를 포함해 caller-owned options를 저장하기 전에 snapshot으로 분리합니다. `ConfigModule.forRoot(...)`, `ConfigReloadModule.forRoot(...)`, `createConfigReloader(...)
|
|
146
|
+
Module registration과 reloader 생성은 `schema`로 전달한 nested Standard Schema validator object를 포함해 caller-owned options를 저장하기 전에 snapshot으로 분리합니다. 이 캡처는 provider resolution이나 application bootstrap보다 앞선 `ConfigModule.forRoot(...)`, `ConfigReloadModule.forRoot(...)`, `createConfigReloader(...)` 호출 시점에 동기적으로 일어납니다. Config dictionary, `processEnv`, Standard Schema descriptor는 분리하고, `parse`, `onReloadError`, schema validator 같은 callable value는 해당 호출에서 캡처한 reference를 유지합니다. 이후 option object나 snapshot으로 분리된 nested object를 변경해도 bootstrap, manual reload, watch reload 입력은 바뀌지 않습니다. `ConfigModule.forRoot({ watch: true, ... })`를 사용하면 module은 application bootstrap 중 env-file watcher를 시작하고, 먼저 injected `ConfigService`를 watch reloader baseline과 맞춘 다음 watch reload가 성공한 뒤 같은 injected `ConfigService` instance를 갱신합니다. `ConfigModule`의 automatic watch reload 실패를 애플리케이션이 소유해야 한다면 `onReloadError`를 전달하세요. Watch mode에서는 기존 env file과 누락된 env file 모두에 대해 parent directory를 watch하므로, 나중에 env file을 생성하거나 atomic replacement로 교체해도 reload가 트리거될 수 있습니다. Watch reload는 reload 전에 최종 env file content를 마지막으로 commit된 watch baseline과 비교하므로, 내용이 바뀌지 않은 저장이나 변경 후 debounce 안에서 원래 내용으로 되돌린 burst는 인프로세스 config snapshot을 교체하지 않습니다.
|
|
105
147
|
|
|
106
|
-
`ConfigReloadModule`은 명시적으로 주입 가능한 reload layer이며 standalone config source가 아닙니다. manual reload나 subscription을 위해 `CONFIG_RELOADER`가 필요한 caller는 `ConfigModule` 또는 다른 `ConfigService` provider와 함께 사용하세요. `ConfigModule` 또는 `ConfigReloadModule`이 만든 watcher는 `watch: true`일 때만 생성되며 module shutdown 중에 닫힙니다. 같은 env file에 대해서는 한 layer에서만 `watch: true`를 활성화하세요. 자동 `ConfigService` 갱신만 필요하면 `ConfigModule`을 사용하고, subscription/manual reload를 위한 injected reloader 계약이 필요하면 `ConfigReloadModule`을 사용합니다.
|
|
148
|
+
`ConfigReloadModule`은 명시적으로 주입 가능한 reload layer이며 standalone config source가 아닙니다. manual reload나 subscription을 위해 `CONFIG_RELOADER`가 필요한 caller는 `ConfigModule` 또는 다른 `ConfigService` provider와 함께 사용하세요. `ConfigModule` 또는 `ConfigReloadModule`이 만든 watcher는 `watch: true`일 때만 생성되며 module shutdown 중에 닫힙니다. `ConfigReloadManager`의 종료는 최종 상태입니다. `close()` 또는 `onModuleDestroy()` 이후에는 대체 reloader나 watcher를 다시 생성하지 않고, `reload()`, `subscribe()`, `subscribeError()`는 `InvariantError`를 던지며, `current()`는 마지막으로 commit된 스냅샷을 계속 반환합니다. 같은 env file에 대해서는 한 layer에서만 `watch: true`를 활성화하세요. 자동 `ConfigService` 갱신만 필요하면 `ConfigModule`을 사용하고, subscription/manual reload를 위한 injected reloader 계약이 필요하면 `ConfigReloadModule`을 사용합니다.
|
|
107
149
|
|
|
108
150
|
## 공개 API
|
|
109
151
|
|
|
@@ -123,14 +165,17 @@ Module registration과 reloader 생성은 `schema`로 전달한 nested Standard
|
|
|
123
165
|
|
|
124
166
|
## 관련 패키지
|
|
125
167
|
|
|
126
|
-
- `@fluojs/runtime`:
|
|
168
|
+
- `@fluojs/runtime`: configuration loading을 transitive하게 제공하지 않습니다. `ConfigModule.forRoot(...)`를 사용하거나 `ConfigService`를 주입하는 애플리케이션은 `@fluojs/config`를 direct dependency로 선언해야 합니다.
|
|
127
169
|
- Standard Schema validator: Zod, Valibot, ArkType 및 호환 schema 라이브러리를 `schema` 옵션으로 전달할 수 있습니다.
|
|
128
170
|
|
|
129
171
|
## 예제 소스
|
|
130
172
|
|
|
131
173
|
- `packages/config/src/load.ts`
|
|
174
|
+
- `packages/config/src/options.ts`
|
|
175
|
+
- `packages/config/src/module.ts`
|
|
132
176
|
- `packages/config/src/service.ts`
|
|
133
177
|
- `packages/config/src/load.test.ts`
|
|
134
178
|
- `packages/config/src/reload-module.ts`
|
|
135
|
-
- `
|
|
136
|
-
-
|
|
179
|
+
- `packages/config/src/reload-module.test.ts`
|
|
180
|
+
- [구성 및 환경](../../docs/architecture/config-and-environments.ko.md)
|
|
181
|
+
- [개발 리로드 아키텍처](../../docs/architecture/dev-reload-architecture.ko.md)
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
Configuration loading, merging, validation, and typed runtime access for fluo applications.
|
|
6
6
|
|
|
7
|
+
Preparing for the coordinated Node 24 release? Follow the [consumer migration guide](../../docs/getting-started/migrate-node24.md) before upgrading packages.
|
|
8
|
+
|
|
7
9
|
## Table of Contents
|
|
8
10
|
|
|
9
11
|
- [Installation](#installation)
|
|
@@ -20,7 +22,7 @@ Configuration loading, merging, validation, and typed runtime access for fluo ap
|
|
|
20
22
|
npm install @fluojs/config
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
The package
|
|
25
|
+
The package intentionally declares no package-wide `engines.node`. `ConfigService`, merge/validation/clone behavior, and `loadConfig({ defaults, processEnv, runtimeOverrides })` are portable and never resolve `process.cwd()`, a default `.env` path, or Node filesystem/path/crypto builtins. Env-file loading, default `.env` loading, and watch mode are Node-only features: they lazily resolve builtins through `process.getBuiltinModule(...)` (supported on Node.js `>=24.0.0 <27`) and throw `CONFIG_RUNTIME_UNAVAILABLE` with guidance to use in-memory options or run that feature on Node.js when the host cannot provide the boundary. This guard checks feature capabilities; it does not compare Node versions or reject portable root imports.
|
|
24
26
|
|
|
25
27
|
## When to Use
|
|
26
28
|
|
|
@@ -59,7 +61,7 @@ const EnvSchema = z.object({
|
|
|
59
61
|
class AppModule {}
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
Use `envFilePath` when the env file lives at an absolute or pre-resolved path, or `parse` when you need a custom flat-file parser instead of the default dotenv parser.
|
|
64
|
+
Use `envFilePath` when the env file lives at an absolute or pre-resolved path, `envFilePaths` when a deployment needs several env files layered in an explicit order, or `parse` when you need a custom flat-file parser instead of the default dotenv parser.
|
|
63
65
|
|
|
64
66
|
Once registered, you can inject `ConfigService` to access your values:
|
|
65
67
|
|
|
@@ -78,6 +80,19 @@ class MyService {
|
|
|
78
80
|
|
|
79
81
|
## Key Capabilities
|
|
80
82
|
|
|
83
|
+
### NestJS Registration Migration
|
|
84
|
+
|
|
85
|
+
`ConfigModule` exposes only synchronous `forRoot(...)` registration and has no `ConfigModule.forRootAsync(...)` counterpart. When migrating from `@nestjs/config`:
|
|
86
|
+
|
|
87
|
+
- Resolve remote secrets and other asynchronous sources at the application-owned bootstrap boundary before defining the module graph, then pass their final values to the synchronous registration call.
|
|
88
|
+
- Replace NestJS `load` factories with nested plain objects in `defaults` or `runtimeOverrides`; keep the nesting intact for deep merging and dot-path `ConfigService` access.
|
|
89
|
+
- Pass an explicit `processEnv` snapshot because `@fluojs/config` does not scan ambient environment variables.
|
|
90
|
+
- Replace NestJS `validate` callbacks with a synchronous Standard Schema passed as `schema`; asynchronous schema results are rejected.
|
|
91
|
+
- Use `global`, not NestJS `isGlobal`. Visibility is global by default, and `global: false` opts into module-local visibility.
|
|
92
|
+
- Rewrite call sites to the single-key shape: `ConfigService.get(key)` and `getOrThrow(key)` take one key and expose no NestJS default-value or options overload. Own defaults in `defaults` or the `schema` output, or apply an explicit `??` fallback to the `get(key)` result.
|
|
93
|
+
|
|
94
|
+
See the canonical [NestJS configuration migration guide](../../docs/getting-started/migrate-from-nestjs.md) for the shared validated-snapshot bootstrap pattern and HTTP adapter boundary.
|
|
95
|
+
|
|
81
96
|
### Source Precedence
|
|
82
97
|
Configuration is merged in the following order (highest precedence wins):
|
|
83
98
|
1. **Runtime Overrides**: Values passed explicitly via `runtimeOverrides`.
|
|
@@ -89,7 +104,34 @@ Configuration is merged in the following order (highest precedence wins):
|
|
|
89
104
|
|
|
90
105
|
`envFilePath` overrides `envFile`, and `parse` lets callers replace dotenv parsing with a custom parser for flat key/value files. Empty load/module options preserve the default `<cwd>/.env` behavior for `loadConfig({})` and `ConfigModule.forRoot()`. Missing env files are treated as empty input during load; watch mode also observes the parent directory so creating the file later can trigger a reload.
|
|
91
106
|
|
|
92
|
-
|
|
107
|
+
### Ordered Multi-File Env Loading
|
|
108
|
+
|
|
109
|
+
`envFilePaths` accepts one explicit, ordered list of env files. The list is merged from lowest to highest precedence into the single env-file tier, so it still sits above `defaults` and below `processEnv` and `runtimeOverrides`.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
ConfigModule.forRoot({
|
|
113
|
+
envFilePaths: ['.env', '.env.production', '.env.production.local'],
|
|
114
|
+
processEnv: {
|
|
115
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
116
|
+
},
|
|
117
|
+
schema: EnvSchema,
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Contract:
|
|
122
|
+
|
|
123
|
+
- Later entries win over earlier entries; plain objects still deep-merge and arrays still replace.
|
|
124
|
+
- Relative entries resolve against `cwd` (defaulting to `process.cwd()`); absolute entries are used as-is.
|
|
125
|
+
- Missing files contribute nothing and never fail the load.
|
|
126
|
+
- `envFilePaths: []` explicitly opts out of env-file loading, including the default `<cwd>/.env` fallback.
|
|
127
|
+
- Combining `envFilePaths` with `envFile` or `envFilePath` fails with `INVALID_CONFIG`, as do duplicate resolved paths and blank entries.
|
|
128
|
+
- The schema validates the fully merged result once, not each file individually.
|
|
129
|
+
- In watch mode every distinct parent directory is watched once, any listed-file change recomputes the entire list, and deleting a higher-precedence file falls back to the remaining files. Validation failures keep the last valid snapshot.
|
|
130
|
+
- Automatic profile discovery stays outside the package: the caller decides the exact list and order.
|
|
131
|
+
|
|
132
|
+
The package does not derive env-file names from `NODE_ENV`. Build the list at the bootstrap boundary when a deployment needs environment-specific layering.
|
|
133
|
+
|
|
134
|
+
Importing the root `@fluojs/config` package is safe for in-memory consumers that only need `ConfigService`, option types, or `loadConfig(...)` with explicit in-memory inputs. Non-Node runtimes are supported for those portable paths. Env-file, default `.env`, and watch execution remain Node-only and require a host with `process.getBuiltinModule(...)`; unsupported hosts receive the documented `CONFIG_RUNTIME_UNAVAILABLE` error instead of an eager import failure.
|
|
93
135
|
|
|
94
136
|
### Deep Merging
|
|
95
137
|
Plain objects are deep-merged by key. Arrays and primitive values from higher-precedence sources completely replace lower-precedence ones.
|
|
@@ -104,9 +146,9 @@ The `schema` option accepts a synchronous [Standard Schema](https://standardsche
|
|
|
104
146
|
|
|
105
147
|
`ConfigReloadManager.reload()` serializes reload work. If another reload is requested while the current reload is notifying listeners, the follow-up reload is queued and applied after the active notification finishes; if the active notification fails, the previous snapshot is restored and the queued reload is discarded. The same serialization and rollback contract applies to `createConfigReloader(...).reload()`, including manual reloads queued during watch-triggered notifications.
|
|
106
148
|
|
|
107
|
-
Module registration and reloader creation snapshot caller-owned options before storing them, including nested Standard Schema validator objects supplied through `schema`.
|
|
149
|
+
Module registration and reloader creation snapshot caller-owned options before storing them, including nested Standard Schema validator objects supplied through `schema`. This capture happens synchronously when `ConfigModule.forRoot(...)`, `ConfigReloadModule.forRoot(...)`, or `createConfigReloader(...)` is called, before provider resolution or application bootstrap. Config dictionaries, `processEnv`, and the Standard Schema descriptor are detached, while callable values such as `parse`, `onReloadError`, and the schema validator remain the references captured at that call. Later mutations to the option object or its snapshotted nested objects do not affect bootstrap, manual reloads, or watch reloads. When `ConfigModule.forRoot({ watch: true, ... })` is used, the module starts an env-file watcher during application bootstrap, first aligns the injected `ConfigService` with the watch reloader baseline, and then updates the same injected `ConfigService` instance after successful watch reloads. Pass `onReloadError` when the application needs ownership of automatic watch reload failures from `ConfigModule`. In watch mode, the parent directory is watched for both existing and missing env files, so creating or atomically replacing the env file can trigger reload. Watch reloads compare the final env file content with the last committed watch baseline before reloading, so unchanged saves and change-then-revert bursts do not replace the in-process config snapshot.
|
|
108
150
|
|
|
109
|
-
`ConfigReloadModule` is the explicit injectable reload layer, not a standalone config source. Pair it with `ConfigModule` or another `ConfigService` provider when callers need `CONFIG_RELOADER` for manual reloads or subscriptions. Watchers created by `ConfigModule` or `ConfigReloadModule` are created only when `watch: true`, and they are closed during module shutdown. Enable `watch: true` on one layer for a given env file: use `ConfigModule` for automatic `ConfigService` updates, or `ConfigReloadModule` when callers need the injected reloader contract for subscriptions/manual reloads.
|
|
151
|
+
`ConfigReloadModule` is the explicit injectable reload layer, not a standalone config source. Pair it with `ConfigModule` or another `ConfigService` provider when callers need `CONFIG_RELOADER` for manual reloads or subscriptions. Watchers created by `ConfigModule` or `ConfigReloadModule` are created only when `watch: true`, and they are closed during module shutdown. `ConfigReloadManager` shutdown is terminal: after `close()` or `onModuleDestroy()`, the manager never creates another reloader or watcher, `reload()`, `subscribe()`, and `subscribeError()` throw an `InvariantError`, and `current()` keeps returning the last committed snapshot. Enable `watch: true` on one layer for a given env file: use `ConfigModule` for automatic `ConfigService` updates, or `ConfigReloadModule` when callers need the injected reloader contract for subscriptions/manual reloads.
|
|
110
152
|
|
|
111
153
|
## Public API
|
|
112
154
|
|
|
@@ -126,14 +168,17 @@ The package also exports option and subscription types such as `ConfigModuleOpti
|
|
|
126
168
|
|
|
127
169
|
## Related Packages
|
|
128
170
|
|
|
129
|
-
- **`@fluojs/runtime`**:
|
|
171
|
+
- **`@fluojs/runtime`**: Does not provide configuration loading transitively. Applications that use `ConfigModule.forRoot(...)` or inject `ConfigService` must declare `@fluojs/config` as a direct dependency.
|
|
130
172
|
- **Standard Schema validators**: Zod, Valibot, ArkType, and other compatible schema libraries can be passed through the `schema` option.
|
|
131
173
|
|
|
132
174
|
## Example Sources
|
|
133
175
|
|
|
134
176
|
- `packages/config/src/load.ts`
|
|
177
|
+
- `packages/config/src/options.ts`
|
|
178
|
+
- `packages/config/src/module.ts`
|
|
135
179
|
- `packages/config/src/service.ts`
|
|
136
180
|
- `packages/config/src/load.test.ts`
|
|
137
181
|
- `packages/config/src/reload-module.ts`
|
|
138
|
-
- `
|
|
139
|
-
-
|
|
182
|
+
- `packages/config/src/reload-module.test.ts`
|
|
183
|
+
- [Config and Environments](../../docs/architecture/config-and-environments.md)
|
|
184
|
+
- [Dev Reload Architecture](../../docs/architecture/dev-reload-architecture.md)
|
package/dist/load.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EAKf,MAAM,YAAY,CAAC;AA0zBpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAiC/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAEvE"}
|
package/dist/load.js
CHANGED
|
@@ -2,7 +2,8 @@ import { FluoError } from '@fluojs/core';
|
|
|
2
2
|
import { cloneConfigDictionary } from './clone.js';
|
|
3
3
|
import { snapshotConfigLoadOptions } from './options.js';
|
|
4
4
|
const reloadFailureReasons = new WeakMap();
|
|
5
|
-
const nodeBuiltinRuntimeRequirement = '
|
|
5
|
+
const nodeBuiltinRuntimeRequirement = 'Supported env-file loading and watch mode in @fluojs/config require Node.js >=24.0.0 <27.';
|
|
6
|
+
const watchEventDebounceMs = 100;
|
|
6
7
|
let requireNodeBuiltin;
|
|
7
8
|
function resolveRequireNodeBuiltin() {
|
|
8
9
|
if (requireNodeBuiltin) {
|
|
@@ -89,7 +90,7 @@ function unquoteEnvValue(value) {
|
|
|
89
90
|
return quote === '"' ? unquoted.replace(/\\n/g, '\n').replace(/\\r/g, '\r') : unquoted;
|
|
90
91
|
}
|
|
91
92
|
function stripInlineEnvComment(value) {
|
|
92
|
-
const commentIndex = value.
|
|
93
|
+
const commentIndex = value.indexOf('#');
|
|
93
94
|
return commentIndex === -1 ? value : value.slice(0, commentIndex);
|
|
94
95
|
}
|
|
95
96
|
function findClosingEnvQuote(value, quote) {
|
|
@@ -222,20 +223,60 @@ function rejectLegacyValidateOption(options) {
|
|
|
222
223
|
});
|
|
223
224
|
}
|
|
224
225
|
}
|
|
226
|
+
function rejectAmbiguousEnvFileOptions(options) {
|
|
227
|
+
if (options.envFilePaths === undefined) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (options.envFile !== undefined || options.envFilePath !== undefined) {
|
|
231
|
+
throw new FluoError('Invalid configuration.', {
|
|
232
|
+
code: 'INVALID_CONFIG',
|
|
233
|
+
cause: new Error('`envFilePaths` cannot be combined with `envFile` or `envFilePath`. Use one explicit ordered list instead of mixing singular and list env-file options.')
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function resolveEnvFilePaths(envFilePaths, cwd) {
|
|
238
|
+
if (envFilePaths.length === 0) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
const path = nodePath();
|
|
242
|
+
const baseDirectory = cwd ?? resolveCurrentWorkingDirectory();
|
|
243
|
+
const resolved = [];
|
|
244
|
+
const seen = new Set();
|
|
245
|
+
for (const entry of envFilePaths) {
|
|
246
|
+
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
247
|
+
throw new FluoError('Invalid configuration.', {
|
|
248
|
+
code: 'INVALID_CONFIG',
|
|
249
|
+
cause: new Error('`envFilePaths` entries must be non-empty file path strings.')
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const resolvedEntry = path.resolve(baseDirectory, entry);
|
|
253
|
+
if (seen.has(resolvedEntry)) {
|
|
254
|
+
throw new FluoError('Invalid configuration.', {
|
|
255
|
+
code: 'INVALID_CONFIG',
|
|
256
|
+
cause: new Error(`\`envFilePaths\` must not repeat the same resolved file. Duplicate entry: ${resolvedEntry}.`)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
seen.add(resolvedEntry);
|
|
260
|
+
resolved.push(resolvedEntry);
|
|
261
|
+
}
|
|
262
|
+
return resolved;
|
|
263
|
+
}
|
|
225
264
|
function normalizeLoadOptions(options) {
|
|
226
265
|
rejectLegacyValidateOption(options);
|
|
227
|
-
|
|
266
|
+
rejectAmbiguousEnvFileOptions(options);
|
|
267
|
+
const hasExplicitEnvFile = options.envFilePath !== undefined || options.envFile !== undefined || options.envFilePaths !== undefined;
|
|
228
268
|
const hasExplicitInMemorySource = options.defaults !== undefined || options.processEnv !== undefined || options.runtimeOverrides !== undefined;
|
|
229
269
|
const shouldUseDefaultEnvFile = !hasExplicitEnvFile && (options.cwd !== undefined || options.watch === true || !hasExplicitInMemorySource);
|
|
230
|
-
const cwd = shouldUseDefaultEnvFile
|
|
231
|
-
const
|
|
270
|
+
const cwd = shouldUseDefaultEnvFile ? options.cwd ?? resolveCurrentWorkingDirectory() : options.cwd;
|
|
271
|
+
const singularEnvFile = options.envFilePath ?? options.envFile ?? (shouldUseDefaultEnvFile && cwd ? nodePath().join(cwd, '.env') : undefined);
|
|
272
|
+
const envFiles = options.envFilePaths === undefined ? singularEnvFile === undefined ? [] : [singularEnvFile] : resolveEnvFilePaths(options.envFilePaths, options.cwd);
|
|
232
273
|
const defaults = options.defaults ?? {};
|
|
233
274
|
const processEnv = options.processEnv ?? {};
|
|
234
275
|
const safeProcessEnv = sanitizeProcessEnv(processEnv);
|
|
235
276
|
const runtimeOverrides = options.runtimeOverrides ?? {};
|
|
236
277
|
return {
|
|
237
278
|
defaults,
|
|
238
|
-
|
|
279
|
+
envFiles,
|
|
239
280
|
parse: options.parse,
|
|
240
281
|
runtimeOverrides,
|
|
241
282
|
safeProcessEnv,
|
|
@@ -243,17 +284,18 @@ function normalizeLoadOptions(options) {
|
|
|
243
284
|
};
|
|
244
285
|
}
|
|
245
286
|
function readEnvFileValues(options) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
287
|
+
const layers = [];
|
|
288
|
+
for (const envFile of options.envFiles) {
|
|
289
|
+
try {
|
|
290
|
+
layers.push(parseEnvContent(nodeFs().readFileSync(envFile, 'utf8'), options.safeProcessEnv, options.parse));
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (isNodeFsError(error) && error.code === 'ENOENT') {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
throw error;
|
|
254
296
|
}
|
|
255
|
-
throw error;
|
|
256
297
|
}
|
|
298
|
+
return layers;
|
|
257
299
|
}
|
|
258
300
|
function isPlainObject(value) {
|
|
259
301
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -281,8 +323,7 @@ function mergeConfigSources(...sources) {
|
|
|
281
323
|
return merged;
|
|
282
324
|
}
|
|
283
325
|
function buildMergedConfig(options) {
|
|
284
|
-
|
|
285
|
-
return mergeConfigSources(options.defaults, envFileValues, options.safeProcessEnv, options.runtimeOverrides);
|
|
326
|
+
return mergeConfigSources(options.defaults, ...readEnvFileValues(options), options.safeProcessEnv, options.runtimeOverrides);
|
|
286
327
|
}
|
|
287
328
|
function isPromiseLike(value) {
|
|
288
329
|
return (typeof value === 'object' || typeof value === 'function') && value !== null && 'then' in value && typeof value.then === 'function';
|
|
@@ -377,6 +418,21 @@ function hashEnvFileContent(envFile) {
|
|
|
377
418
|
throw error;
|
|
378
419
|
}
|
|
379
420
|
}
|
|
421
|
+
function hashEnvFileListContent(envFiles) {
|
|
422
|
+
if (envFiles.length === 0) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
const digest = nodeCrypto().createHash('sha256');
|
|
426
|
+
let hasAnyFile = false;
|
|
427
|
+
for (const envFile of envFiles) {
|
|
428
|
+
const fileHash = hashEnvFileContent(envFile);
|
|
429
|
+
if (fileHash !== undefined) {
|
|
430
|
+
hasAnyFile = true;
|
|
431
|
+
}
|
|
432
|
+
digest.update(`${envFile}\u0000${fileHash ?? ''}\u0000`);
|
|
433
|
+
}
|
|
434
|
+
return hasAnyFile ? digest.digest('hex') : undefined;
|
|
435
|
+
}
|
|
380
436
|
function notifyReloadListeners(listeners, snapshot, reason) {
|
|
381
437
|
for (const listener of listeners) {
|
|
382
438
|
listener(cloneConfigDictionary(snapshot), reason);
|
|
@@ -424,43 +480,78 @@ function applyReload(normalized, state, listeners, reason) {
|
|
|
424
480
|
}
|
|
425
481
|
}
|
|
426
482
|
function startReloaderWatcher(normalized, options, state, listeners, errorListeners) {
|
|
427
|
-
if (!options.watch) {
|
|
428
|
-
return
|
|
429
|
-
}
|
|
430
|
-
if (normalized.envFile === undefined) {
|
|
431
|
-
return undefined;
|
|
483
|
+
if (!options.watch || normalized.envFiles.length === 0) {
|
|
484
|
+
return [];
|
|
432
485
|
}
|
|
433
486
|
const path = nodePath();
|
|
434
487
|
const fs = nodeFs();
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
488
|
+
const watchedNamesByDirectory = new Map();
|
|
489
|
+
for (const envFile of normalized.envFiles) {
|
|
490
|
+
const directory = path.dirname(envFile);
|
|
491
|
+
const watchedNames = watchedNamesByDirectory.get(directory);
|
|
492
|
+
if (watchedNames) {
|
|
493
|
+
watchedNames.add(path.basename(envFile));
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
watchedNamesByDirectory.set(directory, new Set([path.basename(envFile)]));
|
|
440
497
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (filename !== null && filename.toString() !== watchedEnvFileName) {
|
|
445
|
-
return;
|
|
498
|
+
const scheduleWatchReload = () => {
|
|
499
|
+
if (state.watchReloadTimer !== undefined) {
|
|
500
|
+
clearTimeout(state.watchReloadTimer);
|
|
446
501
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
502
|
+
state.watchReloadTimer = setTimeout(() => {
|
|
503
|
+
state.watchReloadTimer = undefined;
|
|
504
|
+
try {
|
|
505
|
+
const nextEnvFileHash = hashEnvFileListContent(normalized.envFiles);
|
|
506
|
+
if (nextEnvFileHash === state.watchedEnvFileHash) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
applyReload(normalized, state, listeners, 'watch');
|
|
510
|
+
state.watchedEnvFileHash = nextEnvFileHash;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
notifyReloadErrorListeners(errorListeners, error, getReloadFailureReason(error) ?? 'watch');
|
|
451
513
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
514
|
+
}, watchEventDebounceMs);
|
|
515
|
+
};
|
|
516
|
+
const watchers = [];
|
|
517
|
+
try {
|
|
518
|
+
for (const [directory, watchedNames] of watchedNamesByDirectory) {
|
|
519
|
+
if (!fs.existsSync(directory)) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
watchers.push(fs.watch(directory, {
|
|
523
|
+
persistent: false
|
|
524
|
+
}, (_eventType, filename) => {
|
|
525
|
+
if (filename !== null && !watchedNames.has(filename.toString())) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
scheduleWatchReload();
|
|
529
|
+
}));
|
|
456
530
|
}
|
|
457
|
-
})
|
|
531
|
+
} catch (error) {
|
|
532
|
+
rollBackStartedWatchers(watchers);
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
return watchers;
|
|
536
|
+
}
|
|
537
|
+
function rollBackStartedWatchers(watchers) {
|
|
538
|
+
for (const watcher of watchers) {
|
|
539
|
+
try {
|
|
540
|
+
watcher.close();
|
|
541
|
+
} catch {
|
|
542
|
+
// A cleanup failure must never mask the watcher initialization error being rethrown.
|
|
543
|
+
}
|
|
544
|
+
}
|
|
458
545
|
}
|
|
459
546
|
function closeReloader(state, listeners, errorListeners) {
|
|
460
|
-
if (state.
|
|
461
|
-
state.
|
|
462
|
-
state.
|
|
547
|
+
if (state.watchReloadTimer !== undefined) {
|
|
548
|
+
clearTimeout(state.watchReloadTimer);
|
|
549
|
+
state.watchReloadTimer = undefined;
|
|
550
|
+
}
|
|
551
|
+
for (const watcher of state.watchers) {
|
|
552
|
+
watcher.close();
|
|
463
553
|
}
|
|
554
|
+
state.watchers = [];
|
|
464
555
|
listeners.clear();
|
|
465
556
|
errorListeners.clear();
|
|
466
557
|
}
|
|
@@ -492,12 +583,13 @@ export function createConfigReloader(options) {
|
|
|
492
583
|
current: resolveConfig(normalized),
|
|
493
584
|
pendingReloadReason: undefined,
|
|
494
585
|
reloading: false,
|
|
495
|
-
watchedEnvFileHash:
|
|
496
|
-
|
|
586
|
+
watchedEnvFileHash: hashEnvFileListContent(normalized.envFiles),
|
|
587
|
+
watchReloadTimer: undefined,
|
|
588
|
+
watchers: []
|
|
497
589
|
};
|
|
498
590
|
const listeners = new Set();
|
|
499
591
|
const errorListeners = new Set();
|
|
500
|
-
state.
|
|
592
|
+
state.watchers = startReloaderWatcher(normalized, loadOptions, state, listeners, errorListeners);
|
|
501
593
|
return {
|
|
502
594
|
close() {
|
|
503
595
|
closeReloader(state, listeners, errorListeners);
|
package/dist/module.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAA4C,MAAM,YAAY,CAAC;AAmDhG;;GAEG;AACH,qBAAa,YAAY;IACvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,UAAU,YAAY;
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAA4C,MAAM,YAAY,CAAC;AAmDhG;;GAEG;AACH,qBAAa,YAAY;IACvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,UAAU,YAAY;CA2BtE"}
|
package/dist/module.js
CHANGED
|
@@ -86,13 +86,10 @@ export class ConfigModule {
|
|
|
86
86
|
const providers = [{
|
|
87
87
|
provide: ConfigService,
|
|
88
88
|
useFactory: () => createConfigServiceFromSnapshot(loadConfig(loadOptions))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
useValue: loadOptions
|
|
94
|
-
}, _ConfigModuleWatchMan);
|
|
95
|
-
}
|
|
89
|
+
}, ...(loadOptions.watch ? [{
|
|
90
|
+
provide: CONFIG_MODULE_WATCH_OPTIONS,
|
|
91
|
+
useValue: loadOptions
|
|
92
|
+
}, _ConfigModuleWatchMan] : [])];
|
|
96
93
|
defineModuleMetadata(ConfigModuleImpl, {
|
|
97
94
|
global: loadOptions.global ?? true,
|
|
98
95
|
exports: [ConfigService],
|
package/dist/options.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAoB,iBAAiB,EAAE,mBAAmB,
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAoB,iBAAiB,EAAE,mBAAmB,EAAkC,MAAM,YAAY,CAAC;AA4C3H;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,CAa9F;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,CAaxF"}
|
package/dist/options.js
CHANGED
|
@@ -2,6 +2,9 @@ import { cloneConfigDictionary } from './clone.js';
|
|
|
2
2
|
function snapshotConfigDictionary(value) {
|
|
3
3
|
return value === undefined ? undefined : cloneConfigDictionary(value);
|
|
4
4
|
}
|
|
5
|
+
function snapshotEnvFilePaths(envFilePaths) {
|
|
6
|
+
return envFilePaths === undefined ? undefined : Object.freeze([...envFilePaths]);
|
|
7
|
+
}
|
|
5
8
|
function snapshotProcessEnv(processEnv) {
|
|
6
9
|
if (processEnv === undefined) {
|
|
7
10
|
return undefined;
|
|
@@ -43,6 +46,7 @@ export function snapshotConfigModuleOptions(options) {
|
|
|
43
46
|
return Object.freeze({
|
|
44
47
|
...options,
|
|
45
48
|
defaults: snapshotConfigDictionary(options.defaults),
|
|
49
|
+
envFilePaths: snapshotEnvFilePaths(options.envFilePaths),
|
|
46
50
|
processEnv: snapshotProcessEnv(options.processEnv),
|
|
47
51
|
runtimeOverrides: snapshotConfigDictionary(options.runtimeOverrides),
|
|
48
52
|
schema: snapshotConfigSchema(options.schema)
|
|
@@ -62,6 +66,7 @@ export function snapshotConfigLoadOptions(options) {
|
|
|
62
66
|
return Object.freeze({
|
|
63
67
|
...options,
|
|
64
68
|
defaults: snapshotConfigDictionary(options.defaults),
|
|
69
|
+
envFilePaths: snapshotEnvFilePaths(options.envFilePaths),
|
|
65
70
|
processEnv: snapshotProcessEnv(options.processEnv),
|
|
66
71
|
runtimeOverrides: snapshotConfigDictionary(options.runtimeOverrides),
|
|
67
72
|
schema: snapshotConfigSchema(options.schema)
|
package/dist/reload-module.d.ts
CHANGED
|
@@ -6,10 +6,18 @@ import type { ConfigDictionary, ConfigLoadOptions, ConfigReloadErrorListener, Co
|
|
|
6
6
|
export declare const CONFIG_RELOADER: unique symbol;
|
|
7
7
|
/**
|
|
8
8
|
* Lazily creates and coordinates the active config reloader instance.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* `close()` and `onModuleDestroy()` are terminal. After shutdown the manager never creates another
|
|
12
|
+
* reloader or watcher, `onApplicationBootstrap()` becomes a no-op, and `current()` keeps returning the
|
|
13
|
+
* last committed `ConfigService` snapshot.
|
|
14
|
+
*
|
|
15
|
+
* @throws {InvariantError} When `reload()`, `subscribe()`, or `subscribeError()` is called after shutdown.
|
|
9
16
|
*/
|
|
10
17
|
export declare class ConfigReloadManager implements ConfigReloader {
|
|
11
18
|
private readonly config;
|
|
12
19
|
private readonly options;
|
|
20
|
+
private closed;
|
|
13
21
|
private reloader;
|
|
14
22
|
private reloadForwarder;
|
|
15
23
|
private errorForwarder;
|
|
@@ -23,6 +31,7 @@ export declare class ConfigReloadManager implements ConfigReloader {
|
|
|
23
31
|
close(): void;
|
|
24
32
|
onApplicationBootstrap(): void;
|
|
25
33
|
onModuleDestroy(): void;
|
|
34
|
+
private assertNotClosed;
|
|
26
35
|
private ensureReloader;
|
|
27
36
|
}
|
|
28
37
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reload-module.d.ts","sourceRoot":"","sources":["../src/reload-module.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,aAAa,EAEd,MAAM,cAAc,CAAC;AACtB,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
|
|
1
|
+
{"version":3,"file":"reload-module.d.ts","sourceRoot":"","sources":["../src/reload-module.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,aAAa,EAEd,MAAM,cAAc,CAAC;AACtB,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;;;;;;;;;GASG;AACH,qBACa,mBAAoB,YAAW,cAAc;IAStD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAT1B,OAAO,CAAC,MAAM,CAAS;IACvB,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;IAM1B,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,wBAAwB;IAMnE,cAAc,CAAC,QAAQ,EAAE,yBAAyB,GAAG,wBAAwB;IAM7E,KAAK,IAAI,IAAI;IAgBb,sBAAsB,IAAI,IAAI;IAQ9B,eAAe,IAAI,IAAI;IAIvB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,cAAc;CA8BvB;AAED;;GAEG;AACH,qBAAa,kBAAkB;IAC7B,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,UAAU,kBAAkB;CAsB1E"}
|
package/dist/reload-module.js
CHANGED
|
@@ -4,7 +4,7 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
4
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
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
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';
|
|
7
|
+
import { Inject, InvariantError } from '@fluojs/core';
|
|
8
8
|
import { defineModuleMetadata } from '@fluojs/core/internal';
|
|
9
9
|
import { cloneConfigDictionary } from './clone.js';
|
|
10
10
|
import { createConfigReloader } from './load.js';
|
|
@@ -27,12 +27,20 @@ function createSubscription(listeners, listener) {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Lazily creates and coordinates the active config reloader instance.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* `close()` and `onModuleDestroy()` are terminal. After shutdown the manager never creates another
|
|
33
|
+
* reloader or watcher, `onApplicationBootstrap()` becomes a no-op, and `current()` keeps returning the
|
|
34
|
+
* last committed `ConfigService` snapshot.
|
|
35
|
+
*
|
|
36
|
+
* @throws {InvariantError} When `reload()`, `subscribe()`, or `subscribeError()` is called after shutdown.
|
|
30
37
|
*/
|
|
31
38
|
let _ConfigReloadManager;
|
|
32
39
|
class ConfigReloadManager {
|
|
33
40
|
static {
|
|
34
41
|
[_ConfigReloadManager, _initClass] = _applyDecs(this, [Inject(ConfigService, CONFIG_RELOAD_OPTIONS)], []).c;
|
|
35
42
|
}
|
|
43
|
+
closed = false;
|
|
36
44
|
reloader;
|
|
37
45
|
reloadForwarder;
|
|
38
46
|
errorForwarder;
|
|
@@ -46,15 +54,19 @@ class ConfigReloadManager {
|
|
|
46
54
|
return this.config.snapshot();
|
|
47
55
|
}
|
|
48
56
|
reload() {
|
|
57
|
+
this.assertNotClosed('reload');
|
|
49
58
|
return this.ensureReloader().reload();
|
|
50
59
|
}
|
|
51
60
|
subscribe(listener) {
|
|
61
|
+
this.assertNotClosed('subscribe');
|
|
52
62
|
return createSubscription(this.reloadListeners, listener);
|
|
53
63
|
}
|
|
54
64
|
subscribeError(listener) {
|
|
65
|
+
this.assertNotClosed('subscribeError');
|
|
55
66
|
return createSubscription(this.errorListeners, listener);
|
|
56
67
|
}
|
|
57
68
|
close() {
|
|
69
|
+
this.closed = true;
|
|
58
70
|
this.reloadForwarder?.unsubscribe();
|
|
59
71
|
this.reloadForwarder = undefined;
|
|
60
72
|
this.errorForwarder?.unsubscribe();
|
|
@@ -67,7 +79,7 @@ class ConfigReloadManager {
|
|
|
67
79
|
this.errorListeners.clear();
|
|
68
80
|
}
|
|
69
81
|
onApplicationBootstrap() {
|
|
70
|
-
if (!this.options.watch) {
|
|
82
|
+
if (this.closed || !this.options.watch) {
|
|
71
83
|
return;
|
|
72
84
|
}
|
|
73
85
|
this.ensureReloader();
|
|
@@ -75,6 +87,11 @@ class ConfigReloadManager {
|
|
|
75
87
|
onModuleDestroy() {
|
|
76
88
|
this.close();
|
|
77
89
|
}
|
|
90
|
+
assertNotClosed(operation) {
|
|
91
|
+
if (this.closed) {
|
|
92
|
+
throw new InvariantError(`Config reload manager cannot ${operation} after shutdown has started.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
78
95
|
ensureReloader() {
|
|
79
96
|
if (this.reloader) {
|
|
80
97
|
return this.reloader;
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
|
3
3
|
* Plain JSON-like object used as the normalized configuration snapshot shape.
|
|
4
4
|
*/
|
|
5
5
|
export type ConfigDictionary = Record<string, unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Structural process-environment snapshot accepted by `@fluojs/config`.
|
|
8
|
+
*
|
|
9
|
+
* Declared by this package instead of the ambient `NodeJS.ProcessEnv` namespace so the
|
|
10
|
+
* published declarations resolve for strict consumers without Node ambient types. A
|
|
11
|
+
* `NodeJS.ProcessEnv` value remains assignable because it has the same structure.
|
|
12
|
+
*/
|
|
13
|
+
export type ConfigProcessEnv = Record<string, string | undefined>;
|
|
6
14
|
/**
|
|
7
15
|
* Standard Schema v1-compatible config validator accepted by `@fluojs/config` loaders.
|
|
8
16
|
*
|
|
@@ -27,7 +35,16 @@ export type DotValue<T, K extends string> = K extends keyof T ? T[K] : K extends
|
|
|
27
35
|
export interface ConfigModuleOptions {
|
|
28
36
|
envFile?: string;
|
|
29
37
|
envFilePath?: string;
|
|
30
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Explicit ordered env-file list merged from lowest to highest precedence.
|
|
40
|
+
*
|
|
41
|
+
* Entries are resolved against `cwd` when relative, missing files contribute nothing,
|
|
42
|
+
* and the merged result stays below `processEnv` and `runtimeOverrides`. Combining this
|
|
43
|
+
* option with `envFile` or `envFilePath`, repeating a resolved path, or passing a blank
|
|
44
|
+
* entry fails with `INVALID_CONFIG`. An empty list opts out of env-file loading entirely.
|
|
45
|
+
*/
|
|
46
|
+
envFilePaths?: readonly string[];
|
|
47
|
+
processEnv?: ConfigProcessEnv;
|
|
31
48
|
schema?: ConfigSchema;
|
|
32
49
|
defaults?: ConfigDictionary;
|
|
33
50
|
/** Highest-precedence values applied after defaults, env files, and `processEnv`. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,SAAS,gBAAgB,GAAG,gBAAgB,IAAI,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAEhI;;;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,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"environment",
|
|
9
9
|
"typed-config"
|
|
10
10
|
],
|
|
11
|
-
"version": "
|
|
11
|
+
"version": "2.0.0",
|
|
12
12
|
"private": false,
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
@@ -16,9 +16,6 @@
|
|
|
16
16
|
"url": "https://github.com/fluojs/fluo.git",
|
|
17
17
|
"directory": "packages/config"
|
|
18
18
|
},
|
|
19
|
-
"engines": {
|
|
20
|
-
"node": ">=20.16.0"
|
|
21
|
-
},
|
|
22
19
|
"publishConfig": {
|
|
23
20
|
"access": "public"
|
|
24
21
|
},
|
|
@@ -36,11 +33,11 @@
|
|
|
36
33
|
],
|
|
37
34
|
"dependencies": {
|
|
38
35
|
"@standard-schema/spec": "^1.1.0",
|
|
39
|
-
"@fluojs/core": "^
|
|
36
|
+
"@fluojs/core": "^2.0.0"
|
|
40
37
|
},
|
|
41
38
|
"devDependencies": {
|
|
42
|
-
"vitest": "^
|
|
43
|
-
"@fluojs/di": "^
|
|
39
|
+
"vitest": "^4.1.11",
|
|
40
|
+
"@fluojs/di": "^3.0.0"
|
|
44
41
|
},
|
|
45
42
|
"scripts": {
|
|
46
43
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|