@fluojs/config 1.0.3 → 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 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
- 패키지는 Node.js 20.16.0 이상을 지원합니다. Env-file loading, 기본 `.env` loading, watch mode는 Node filesystem, path, crypto builtin을 host runtime boundary를 통해 lazy하게 해석하며 `process.getBuiltinModule(...)`을 요구합니다. API가 존재하지만 filesystem/path/crypto direct lookup이 불가능할 때는 published ESM과 호환되는 `node:module` fallback을 사용합니다. `ConfigService`와 `loadConfig({ defaults, processEnv, runtimeOverrides })`의 in-memory 사용은 env-file access를 요구하지 않지만, 배포 계약은 package-level Node.js 20.16.0 engine을 따릅니다.
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
- Root `@fluojs/config` 패키지를 import하는 것만으로는 Node filesystem, path, crypto builtin을 해석하지 않습니다. `ConfigService`, option type, 또는 명시적 in-memory 입력을 쓰는 `loadConfig(...)` consumer는 root import를 안전하게 사용할 수 있고, Node builtin은 env-file load 또는 watch mode가 실제로 실행될 때 lazy하게 해석됩니다. `loadConfig({ defaults, processEnv, runtimeOverrides })`는 `process.cwd()`, 기본 `.env` path, Node filesystem/path/crypto builtin을 해석하지 않습니다. Published package engine이 Node.js 20.16.0 이상이므로 Node.js 20.0.0부터 20.15.x까지와 Node.js 밖의 runtime은 env-file, 기본 `.env`, watch execution path의 지원 package contract에 포함되지 않습니다.
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 생성은 caller-owned options를 저장하기 전에 snapshot으로 분리합니다. `ConfigModule.forRoot(...)`, `ConfigReloadModule.forRoot(...)`, `createConfigReloader(...)`에 넘긴 객체를 나중에 변경해도 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을 교체하지 않습니다.
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 생성은 caller-owned options를 저장하기
123
165
 
124
166
  ## 관련 패키지
125
167
 
126
- - `@fluojs/runtime`: 부트스트랩 `loadConfig()`를 호출합니다.
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
- - `docs/architecture/config-and-environments.md`
136
- - `docs/architecture/dev-reload-architecture.md`
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 supports Node.js 20.16.0 or newer. Env-file loading, default `.env` loading, and watch mode resolve Node filesystem, path, and crypto builtins lazily through the host runtime boundary, requiring `process.getBuiltinModule(...)`; when that API is present but direct filesystem/path/crypto lookup is unavailable, a published-ESM-compatible `node:module` fallback is used. In-memory use of `ConfigService` and `loadConfig({ defaults, processEnv, runtimeOverrides })` does not require env-file access, but it is still distributed under the package-level Node.js 20.16.0 engine contract.
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
- 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: Node filesystem, path, and crypto builtins are resolved lazily only when env-file loading or watch mode actually runs. `loadConfig({ defaults, processEnv, runtimeOverrides })` does not resolve `process.cwd()`, a default `.env` path, or Node filesystem/path/crypto builtins. Because the published package engine is Node.js 20.16.0 or newer, Node.js 20.0.0 through 20.15.x and non-Node runtimes are outside the supported package contract for env-file, default `.env`, and watch execution paths.
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. Later mutations to objects passed to `ConfigModule.forRoot(...)`, `ConfigReloadModule.forRoot(...)`, or `createConfigReloader(...)` 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.
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`**: Calls `loadConfig` internally during application bootstrap.
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
- - `docs/architecture/config-and-environments.md`
139
- - `docs/architecture/dev-reload-architecture.md`
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)
@@ -1 +1 @@
1
- {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,cAAc,EAKf,MAAM,YAAY,CAAC;AAisBpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAgC/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAEvE"}
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 = 'Node.js 20.16.0 or newer is required when @fluojs/config loads env files or starts watch mode.';
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.search(/\s#/);
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
- const hasExplicitEnvFile = options.envFilePath !== undefined || options.envFile !== undefined;
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 && options.envFilePath === undefined && options.envFile === undefined ? options.cwd ?? resolveCurrentWorkingDirectory() : options.cwd;
231
- const envFile = options.envFilePath ?? options.envFile ?? (cwd ? nodePath().join(cwd, '.env') : undefined);
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
- envFile,
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
- if (options.envFile === undefined) {
247
- return {};
248
- }
249
- try {
250
- return parseEnvContent(nodeFs().readFileSync(options.envFile, 'utf8'), options.safeProcessEnv, options.parse);
251
- } catch (error) {
252
- if (isNodeFsError(error) && error.code === 'ENOENT') {
253
- return {};
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
- const envFileValues = readEnvFileValues(options);
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';
@@ -337,7 +378,7 @@ function readConfigSchemaResult(result) {
337
378
  if (!isConfigSchemaSuccessResult(result)) {
338
379
  throw createInvalidConfigError(new Error('Standard Schema config validator returned a malformed result.'));
339
380
  }
340
- return result.value;
381
+ return cloneConfigDictionary(result.value);
341
382
  }
342
383
  function validateConfig(options, merged) {
343
384
  if (!options.schema) {
@@ -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 undefined;
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 envFile = normalized.envFile;
436
- const watchTarget = path.dirname(envFile);
437
- const watchedEnvFileName = path.basename(envFile);
438
- if (!fs.existsSync(watchTarget)) {
439
- return undefined;
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
- return fs.watch(watchTarget, {
442
- persistent: false
443
- }, (_eventType, filename) => {
444
- if (filename !== null && filename.toString() !== watchedEnvFileName) {
445
- return;
498
+ const scheduleWatchReload = () => {
499
+ if (state.watchReloadTimer !== undefined) {
500
+ clearTimeout(state.watchReloadTimer);
446
501
  }
447
- try {
448
- const nextEnvFileHash = hashEnvFileContent(envFile);
449
- if (nextEnvFileHash === state.watchedEnvFileHash) {
450
- return;
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
- applyReload(normalized, state, listeners, 'watch');
453
- state.watchedEnvFileHash = nextEnvFileHash;
454
- } catch (error) {
455
- notifyReloadErrorListeners(errorListeners, error, getReloadFailureReason(error) ?? 'watch');
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.watcher) {
461
- state.watcher.close();
462
- state.watcher = undefined;
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: normalized.envFile === undefined ? undefined : hashEnvFileContent(normalized.envFile),
496
- watcher: undefined
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.watcher = startReloaderWatcher(normalized, loadOptions, state, listeners, errorListeners);
592
+ state.watchers = startReloaderWatcher(normalized, loadOptions, state, listeners, errorListeners);
501
593
  return {
502
594
  close() {
503
595
  closeReloader(state, listeners, errorListeners);
@@ -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;CA4BtE"}
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
- if (loadOptions.watch) {
91
- providers.push({
92
- provide: CONFIG_MODULE_WATCH_OPTIONS,
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 CHANGED
@@ -3,14 +3,14 @@ import type { ConfigLoadOptions, ConfigModuleOptions } from './types.js';
3
3
  * Creates a detached snapshot of config module registration options.
4
4
  *
5
5
  * @param options Caller-owned module options captured at registration time.
6
- * @returns Options that cannot observe later caller mutations of config dictionaries.
6
+ * @returns Options that cannot observe later caller mutations of config dictionaries or schema objects.
7
7
  */
8
8
  export declare function snapshotConfigModuleOptions(options?: ConfigModuleOptions): ConfigModuleOptions;
9
9
  /**
10
10
  * Creates a detached snapshot of config load and reload options.
11
11
  *
12
12
  * @param options Caller-owned load options captured by loaders or reload modules.
13
- * @returns Options that preserve registration-time config dictionary inputs.
13
+ * @returns Options that preserve registration-time config dictionary and schema inputs.
14
14
  */
15
15
  export declare function snapshotConfigLoadOptions(options?: ConfigLoadOptions): ConfigLoadOptions;
16
16
  //# sourceMappingURL=options.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAoB,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAsB3F;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,CAU9F;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,CAWxF"}
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;
@@ -14,12 +17,27 @@ function snapshotProcessEnv(processEnv) {
14
17
  }
15
18
  return Object.freeze(snapshot);
16
19
  }
20
+ function snapshotConfigSchema(schema) {
21
+ if (schema === undefined) {
22
+ return undefined;
23
+ }
24
+ const standard = schema['~standard'];
25
+ const snapshot = {
26
+ '~standard': Object.freeze({
27
+ types: standard.types,
28
+ validate: standard.validate,
29
+ vendor: standard.vendor,
30
+ version: standard.version
31
+ })
32
+ };
33
+ return Object.freeze(snapshot);
34
+ }
17
35
 
18
36
  /**
19
37
  * Creates a detached snapshot of config module registration options.
20
38
  *
21
39
  * @param options Caller-owned module options captured at registration time.
22
- * @returns Options that cannot observe later caller mutations of config dictionaries.
40
+ * @returns Options that cannot observe later caller mutations of config dictionaries or schema objects.
23
41
  */
24
42
  export function snapshotConfigModuleOptions(options) {
25
43
  if (options === undefined) {
@@ -28,7 +46,10 @@ export function snapshotConfigModuleOptions(options) {
28
46
  return Object.freeze({
29
47
  ...options,
30
48
  defaults: snapshotConfigDictionary(options.defaults),
31
- processEnv: snapshotProcessEnv(options.processEnv)
49
+ envFilePaths: snapshotEnvFilePaths(options.envFilePaths),
50
+ processEnv: snapshotProcessEnv(options.processEnv),
51
+ runtimeOverrides: snapshotConfigDictionary(options.runtimeOverrides),
52
+ schema: snapshotConfigSchema(options.schema)
32
53
  });
33
54
  }
34
55
 
@@ -36,7 +57,7 @@ export function snapshotConfigModuleOptions(options) {
36
57
  * Creates a detached snapshot of config load and reload options.
37
58
  *
38
59
  * @param options Caller-owned load options captured by loaders or reload modules.
39
- * @returns Options that preserve registration-time config dictionary inputs.
60
+ * @returns Options that preserve registration-time config dictionary and schema inputs.
40
61
  */
41
62
  export function snapshotConfigLoadOptions(options) {
42
63
  if (options === undefined) {
@@ -45,7 +66,9 @@ export function snapshotConfigLoadOptions(options) {
45
66
  return Object.freeze({
46
67
  ...options,
47
68
  defaults: snapshotConfigDictionary(options.defaults),
69
+ envFilePaths: snapshotEnvFilePaths(options.envFilePaths),
48
70
  processEnv: snapshotProcessEnv(options.processEnv),
49
- runtimeOverrides: snapshotConfigDictionary(options.runtimeOverrides)
71
+ runtimeOverrides: snapshotConfigDictionary(options.runtimeOverrides),
72
+ schema: snapshotConfigSchema(options.schema)
50
73
  });
51
74
  }
@@ -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;;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"}
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"}
@@ -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,9 +35,20 @@ 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
- processEnv?: NodeJS.ProcessEnv;
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;
50
+ /** Highest-precedence values applied after defaults, env files, and `processEnv`. */
51
+ runtimeOverrides?: ConfigDictionary;
33
52
  /** Supply a custom file parser (e.g. for YAML or TOML). Receives raw file content,
34
53
  * returns a flat key-value record. Defaults to dotenv parsing. */
35
54
  parse?: (content: string) => Record<string, string>;
@@ -44,7 +63,6 @@ export interface ConfigModuleOptions {
44
63
  */
45
64
  export interface ConfigLoadOptions extends ConfigModuleOptions {
46
65
  cwd?: string;
47
- runtimeOverrides?: ConfigDictionary;
48
66
  }
49
67
  /**
50
68
  * Reason attached to config reload notifications.
@@ -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,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC/B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,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,wEAAwE;IACxE,aAAa,CAAC,EAAE,yBAAyB,CAAC;IAC1C,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;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"}
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;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAElE;;;;;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;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;uEACmE;IACnE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wEAAwE;IACxE,aAAa,CAAC,EAAE,yBAAyB,CAAC;IAC1C,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;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/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "environment",
9
9
  "typed-config"
10
10
  ],
11
- "version": "1.0.3",
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": "^1.0.3"
36
+ "@fluojs/core": "^2.0.0"
40
37
  },
41
38
  "devDependencies": {
42
- "vitest": "^3.2.4",
43
- "@fluojs/di": "^1.1.0"
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",