@fluojs/config 1.0.0-beta.3 → 1.0.0-beta.5

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
@@ -32,6 +32,12 @@ npm install @fluojs/config
32
32
  ```ts
33
33
  import { ConfigModule } from '@fluojs/config';
34
34
  import { Module } from '@fluojs/core';
35
+ import { z } from 'zod';
36
+
37
+ const EnvSchema = z.object({
38
+ DATABASE_URL: z.string().url(),
39
+ PORT: z.coerce.number().default(3000),
40
+ });
35
41
 
36
42
  @Module({
37
43
  imports: [
@@ -41,10 +47,7 @@ import { Module } from '@fluojs/core';
41
47
  DATABASE_URL: process.env.DATABASE_URL,
42
48
  },
43
49
  defaults: { PORT: '3000' },
44
- validate: (config) => {
45
- if (!config.DATABASE_URL) throw new Error('DATABASE_URL이 필요합니다');
46
- return config;
47
- },
50
+ schema: EnvSchema,
48
51
  }),
49
52
  ],
50
53
  })
@@ -78,7 +81,9 @@ class MyService {
78
81
 
79
82
  ### 부트스트랩 전 검증
80
83
 
81
- `validate` 함수는 모든 소스가 합쳐진 뒤 실행되며, 에러를 던지면 부트스트랩이 즉시 중단됩니다.
84
+ `schema` 옵션은 Zod, Valibot, ArkType 같은 동기식 [Standard Schema](https://standardschema.dev/schema) 호환 validator를 받습니다. 스키마는 모든 소스가 합쳐진 뒤 실행되고, 검증된 `value`가 최종 config snapshot이 됩니다. schema issue가 보고되면 bootstrap/load/reload는 `INVALID_CONFIG`로 실패합니다.
85
+
86
+ `@fluojs/config`의 load와 reload API는 동기식입니다. 비동기 Standard Schema 결과는 `INVALID_CONFIG`로 거부되므로 config 검증에는 동기 스키마를 사용하세요.
82
87
 
83
88
  ### 런타임 접근과 리로드 비용 모델
84
89
 
@@ -86,6 +91,8 @@ class MyService {
86
91
 
87
92
  `ConfigReloadManager.reload()`는 리로드 작업을 직렬화합니다. 현재 리로드가 listener 알림을 수행하는 동안 다른 리로드가 요청되면 후속 리로드는 큐에 들어가 활성 알림이 끝난 뒤 적용됩니다. 활성 알림이 실패하면 이전 snapshot을 복구하고 큐에 있던 리로드는 폐기합니다. 동일한 직렬화와 rollback 계약은 `createConfigReloader(...).reload()`에도 적용되며, watch로 시작된 알림 중 큐에 들어간 manual reload도 이 계약을 따릅니다.
88
93
 
94
+ Module registration과 reloader 생성은 caller-owned options를 저장하기 전에 snapshot으로 분리합니다. `ConfigModule.forRoot(...)`, `ConfigReloadModule.forRoot(...)`, `createConfigReloader(...)`에 넘긴 객체를 나중에 변경해도 bootstrap, manual reload, watch reload 입력은 바뀌지 않습니다. Watch mode에서 시작 시점에 env file이 없으면 빈 file snapshot처럼 취급하고 parent directory를 watch하므로, 나중에 env file을 생성해도 reload가 트리거될 수 있습니다.
95
+
89
96
  ## 공개 API
90
97
 
91
98
  | 클래스/헬퍼 | 설명 |
@@ -103,7 +110,7 @@ class MyService {
103
110
  ## 관련 패키지
104
111
 
105
112
  - `@fluojs/runtime`: 부트스트랩 중 `loadConfig()`를 호출합니다.
106
- - `@fluojs/validation`: `validate` 함수 안에서 스키마 기반 검증을 조합할 수 있습니다.
113
+ - Standard Schema validator: Zod, Valibot, ArkType 호환 schema 라이브러리를 `schema` 옵션으로 전달할 수 있습니다.
107
114
 
108
115
  ## 예제 소스
109
116
 
package/README.md CHANGED
@@ -35,6 +35,12 @@ The `ConfigModule` handles loading and validating your configuration during boot
35
35
  ```typescript
36
36
  import { Module } from '@fluojs/core';
37
37
  import { ConfigModule } from '@fluojs/config';
38
+ import { z } from 'zod';
39
+
40
+ const EnvSchema = z.object({
41
+ DATABASE_URL: z.string().url(),
42
+ PORT: z.coerce.number().default(3000),
43
+ });
38
44
 
39
45
  @Module({
40
46
  imports: [
@@ -44,10 +50,7 @@ import { ConfigModule } from '@fluojs/config';
44
50
  DATABASE_URL: process.env.DATABASE_URL,
45
51
  },
46
52
  defaults: { PORT: '3000' },
47
- validate: (config) => {
48
- if (!config.DATABASE_URL) throw new Error('DATABASE_URL is required');
49
- return config;
50
- },
53
+ schema: EnvSchema,
51
54
  }),
52
55
  ],
53
56
  })
@@ -83,13 +86,17 @@ Configuration is merged in the following order (highest precedence wins):
83
86
  Plain objects are deep-merged by key. Arrays and primitive values from higher-precedence sources completely replace lower-precedence ones.
84
87
 
85
88
  ### Validation
86
- The `validate` function runs after all sources are merged but before the application starts. If it throws, the application bootstrap fails immediately.
89
+ The `schema` option accepts a synchronous [Standard Schema](https://standardschema.dev/schema)-compatible validator such as Zod, Valibot, or ArkType. The schema runs after all sources are merged but before the application starts. Its validated `value` becomes the final config snapshot, and reported issues fail bootstrap/load/reload with `INVALID_CONFIG`.
90
+
91
+ `@fluojs/config` keeps loading and reload APIs synchronous. Async Standard Schema results are rejected with `INVALID_CONFIG`; use a synchronous schema for config validation.
87
92
 
88
93
  ### Runtime Access and Reload Cost Model
89
94
  `ConfigService.get('a.b.c')` resolves dot-path keys by walking each path segment, so lookup cost is proportional to path depth. When `get()`, `getOrThrow()`, or `snapshot()` returns an object-like value, the returned value is a detached clone; clone cost is proportional to the returned subtree size so caller mutations cannot affect the active config snapshot.
90
95
 
91
96
  `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.
92
97
 
98
+ 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. In watch mode, a missing env file at startup is treated as an empty file snapshot while the parent directory is watched so creating the env file later can still trigger reload.
99
+
93
100
  ## Public API
94
101
 
95
102
  | Class/Helper | Description |
@@ -107,7 +114,7 @@ The `validate` function runs after all sources are merged but before the applica
107
114
  ## Related Packages
108
115
 
109
116
  - **`@fluojs/runtime`**: Calls `loadConfig` internally during application bootstrap.
110
- - **`@fluojs/validation`**: Can be used within the `validate` function for schema-based validation.
117
+ - **Standard Schema validators**: Zod, Valibot, ArkType, and other compatible schema libraries can be passed through the `schema` option.
111
118
 
112
119
  ## Example Sources
113
120
 
package/dist/clone.d.ts CHANGED
@@ -1,2 +1,8 @@
1
+ /**
2
+ * Clone config dictionary.
3
+ *
4
+ * @param value The value.
5
+ * @returns The clone config dictionary result.
6
+ */
1
7
  export declare function cloneConfigDictionary<T>(value: T): T;
2
8
  //# sourceMappingURL=clone.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../src/clone.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAMpD"}
1
+ {"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../src/clone.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAMpD"}
package/dist/clone.js CHANGED
@@ -1,4 +1,11 @@
1
1
  import { fallbackClone } from '@fluojs/core/internal';
2
+
3
+ /**
4
+ * Clone config dictionary.
5
+ *
6
+ * @param value The value.
7
+ * @returns The clone config dictionary result.
8
+ */
2
9
  export function cloneConfigDictionary(value) {
3
10
  try {
4
11
  return structuredClone(value);
package/dist/load.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { ConfigDictionary, ConfigLoadOptions, ConfigReloader } from './type
2
2
  /**
3
3
  * Creates a stateful config reloader that mirrors `loadConfig(...)` semantics and optionally watches the env file.
4
4
  *
5
- * @param options Configuration loading options, including optional watch mode and validation hooks.
5
+ * @param options Configuration loading options, including optional watch mode and a synchronous Standard Schema validator.
6
6
  * @returns A reloader that exposes the current snapshot, manual reload, subscriptions, and cleanup.
7
7
  * @throws {FluoError} When the initial config load or validation fails.
8
8
  *
@@ -25,7 +25,7 @@ export declare function createConfigReloader(options: ConfigLoadOptions): Config
25
25
  *
26
26
  * Merge precedence stays aligned with the package README contract: `defaults` < env file < `processEnv` < `runtimeOverrides`.
27
27
  *
28
- * @param options Configuration loading options for source precedence, parsing, and validation.
28
+ * @param options Configuration loading options for source precedence, parsing, and synchronous schema validation.
29
29
  * @returns A detached normalized configuration dictionary for the current load.
30
30
  * @throws {FluoError} When validation throws or the config cannot be normalized.
31
31
  */
@@ -1 +1 @@
1
- {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,cAAc,EAIf,MAAM,YAAY,CAAC;AAqQpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CA8B/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":"AAUA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,cAAc,EAKf,MAAM,YAAY,CAAC;AAqYpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CA+B/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAEvE"}
package/dist/load.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readFileSync, watch } from 'node:fs';
2
- import { join } from 'node:path';
2
+ import { basename, dirname, join } from 'node:path';
3
3
  import { FluoError } from '@fluojs/core';
4
4
  import { parse as dotenvParse } from 'dotenv';
5
5
  import { expand as dotenvExpand } from 'dotenv-expand';
6
6
  import { cloneConfigDictionary } from './clone.js';
7
+ import { snapshotConfigLoadOptions } from './options.js';
7
8
  const reloadFailureReasons = new WeakMap();
8
9
  function markReloadFailure(error, reason) {
9
10
  if (typeof error === 'object' && error !== null) {
@@ -32,7 +33,16 @@ function parseEnvContent(content, safeProcessEnv, customParser) {
32
33
  function sanitizeProcessEnv(processEnv) {
33
34
  return Object.fromEntries(Object.entries(processEnv).filter(entry => entry[1] !== undefined));
34
35
  }
36
+ function rejectLegacyValidateOption(options) {
37
+ if ('validate' in options) {
38
+ throw new FluoError('Invalid configuration.', {
39
+ code: 'INVALID_CONFIG',
40
+ cause: new Error('The legacy `validate` option was removed. Use `schema` with a synchronous Standard Schema validator instead.')
41
+ });
42
+ }
43
+ }
35
44
  function normalizeLoadOptions(options) {
45
+ rejectLegacyValidateOption(options);
36
46
  const cwd = options.cwd ?? process.cwd();
37
47
  const envFile = options.envFilePath ?? options.envFile ?? join(cwd, '.env');
38
48
  const defaults = options.defaults ?? {};
@@ -45,7 +55,7 @@ function normalizeLoadOptions(options) {
45
55
  parse: options.parse,
46
56
  runtimeOverrides,
47
57
  safeProcessEnv,
48
- validate: options.validate
58
+ schema: options.schema
49
59
  };
50
60
  }
51
61
  function readEnvFileValues(options) {
@@ -87,14 +97,76 @@ function buildMergedConfig(options) {
87
97
  const envFileValues = readEnvFileValues(options);
88
98
  return mergeConfigSources(options.defaults, envFileValues, options.safeProcessEnv, options.runtimeOverrides);
89
99
  }
100
+ function isPromiseLike(value) {
101
+ return (typeof value === 'object' || typeof value === 'function') && value !== null && 'then' in value && typeof value.then === 'function';
102
+ }
103
+ function isConfigSchemaIssue(value) {
104
+ return typeof value === 'object' && value !== null && 'message' in value && typeof value.message === 'string';
105
+ }
106
+ function isConfigSchemaFailureResult(value) {
107
+ if (typeof value !== 'object' || value === null || !('issues' in value)) {
108
+ return false;
109
+ }
110
+ return Array.isArray(value.issues) && value.issues.every(isConfigSchemaIssue);
111
+ }
112
+ function isConfigSchemaSuccessResult(value) {
113
+ return typeof value === 'object' && value !== null && 'value' in value && isPlainObject(value.value);
114
+ }
115
+ function isConfigSchemaPathKeySegment(value) {
116
+ if (typeof value !== 'object' || value === null || !('key' in value)) {
117
+ return false;
118
+ }
119
+ return typeof value.key === 'string' || typeof value.key === 'number' || typeof value.key === 'symbol';
120
+ }
121
+ function formatConfigSchemaPathSegment(segment) {
122
+ if (typeof segment === 'string' || typeof segment === 'number') {
123
+ return String(segment);
124
+ }
125
+ if (isConfigSchemaPathKeySegment(segment)) {
126
+ return String(segment.key);
127
+ }
128
+ return undefined;
129
+ }
130
+ function formatConfigSchemaIssue(issue) {
131
+ const path = issue.path?.map(formatConfigSchemaPathSegment).filter(segment => segment !== undefined).join('.');
132
+ return path && path.length > 0 ? `${path}: ${issue.message}` : issue.message;
133
+ }
134
+ function createInvalidConfigError(cause, issues) {
135
+ return new FluoError('Invalid configuration.', {
136
+ code: 'INVALID_CONFIG',
137
+ cause,
138
+ meta: issues ? {
139
+ issues: issues.map(formatConfigSchemaIssue)
140
+ } : undefined
141
+ });
142
+ }
143
+ function isInvalidConfigError(error) {
144
+ return error instanceof FluoError && error.code === 'INVALID_CONFIG';
145
+ }
146
+ function readConfigSchemaResult(result) {
147
+ if (isConfigSchemaFailureResult(result)) {
148
+ throw createInvalidConfigError(new Error('Standard Schema config validation failed.'), result.issues);
149
+ }
150
+ if (!isConfigSchemaSuccessResult(result)) {
151
+ throw createInvalidConfigError(new Error('Standard Schema config validator returned a malformed result.'));
152
+ }
153
+ return result.value;
154
+ }
90
155
  function validateConfig(options, merged) {
156
+ if (!options.schema) {
157
+ return merged;
158
+ }
91
159
  try {
92
- return options.validate ? options.validate(merged) : merged;
160
+ const result = options.schema['~standard'].validate(merged);
161
+ if (isPromiseLike(result)) {
162
+ throw new Error('Config schemas must validate synchronously. Async Standard Schema validation is not supported by the synchronous config API.');
163
+ }
164
+ return readConfigSchemaResult(result);
93
165
  } catch (error) {
94
- throw new FluoError('Invalid configuration.', {
95
- code: 'INVALID_CONFIG',
96
- cause: error
97
- });
166
+ if (isInvalidConfigError(error)) {
167
+ throw error;
168
+ }
169
+ throw createInvalidConfigError(error);
98
170
  }
99
171
  }
100
172
  function resolveConfig(options) {
@@ -155,12 +227,20 @@ function applyReload(normalized, state, listeners, reason) {
155
227
  }
156
228
  }
157
229
  function startReloaderWatcher(normalized, options, state, listeners, errorListeners) {
158
- if (!options.watch || !existsSync(normalized.envFile)) {
230
+ if (!options.watch) {
159
231
  return undefined;
160
232
  }
161
- return watch(normalized.envFile, {
233
+ const watchTarget = existsSync(normalized.envFile) ? normalized.envFile : dirname(normalized.envFile);
234
+ const watchedEnvFileName = basename(normalized.envFile);
235
+ if (!existsSync(watchTarget)) {
236
+ return undefined;
237
+ }
238
+ return watch(watchTarget, {
162
239
  persistent: false
163
- }, () => {
240
+ }, (_eventType, filename) => {
241
+ if (watchTarget !== normalized.envFile && filename !== null && filename.toString() !== watchedEnvFileName) {
242
+ return;
243
+ }
164
244
  try {
165
245
  applyReload(normalized, state, listeners, 'watch');
166
246
  } catch (error) {
@@ -180,7 +260,7 @@ function closeReloader(state, listeners, errorListeners) {
180
260
  /**
181
261
  * Creates a stateful config reloader that mirrors `loadConfig(...)` semantics and optionally watches the env file.
182
262
  *
183
- * @param options Configuration loading options, including optional watch mode and validation hooks.
263
+ * @param options Configuration loading options, including optional watch mode and a synchronous Standard Schema validator.
184
264
  * @returns A reloader that exposes the current snapshot, manual reload, subscriptions, and cleanup.
185
265
  * @throws {FluoError} When the initial config load or validation fails.
186
266
  *
@@ -198,7 +278,8 @@ function closeReloader(state, listeners, errorListeners) {
198
278
  * ```
199
279
  */
200
280
  export function createConfigReloader(options) {
201
- const normalized = normalizeLoadOptions(options);
281
+ const loadOptions = snapshotConfigLoadOptions(options);
282
+ const normalized = normalizeLoadOptions(loadOptions);
202
283
  const state = {
203
284
  current: resolveConfig(normalized),
204
285
  pendingReloadReason: undefined,
@@ -207,7 +288,7 @@ export function createConfigReloader(options) {
207
288
  };
208
289
  const listeners = new Set();
209
290
  const errorListeners = new Set();
210
- state.watcher = startReloaderWatcher(normalized, options, state, listeners, errorListeners);
291
+ state.watcher = startReloaderWatcher(normalized, loadOptions, state, listeners, errorListeners);
211
292
  return {
212
293
  close() {
213
294
  closeReloader(state, listeners, errorListeners);
@@ -232,7 +313,7 @@ export function createConfigReloader(options) {
232
313
  *
233
314
  * Merge precedence stays aligned with the package README contract: `defaults` < env file < `processEnv` < `runtimeOverrides`.
234
315
  *
235
- * @param options Configuration loading options for source precedence, parsing, and validation.
316
+ * @param options Configuration loading options for source precedence, parsing, and synchronous schema validation.
236
317
  * @returns A detached normalized configuration dictionary for the current load.
237
318
  * @throws {FluoError} When validation throws or the config cannot be normalized.
238
319
  */
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD;;GAEG;AACH,qBAAa,YAAY;IACvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,UAAU,YAAY;CAgBtE"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD;;GAEG;AACH,qBAAa,YAAY;IACvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,UAAU,YAAY;CAiBtE"}
package/dist/module.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { defineModuleMetadata } from '@fluojs/core/internal';
2
2
  import { loadConfig } from './load.js';
3
+ import { snapshotConfigModuleOptions } from './options.js';
3
4
  import { ConfigService, createConfigServiceFromSnapshot } from './service.js';
4
5
  /**
5
6
  * Module facade that wires normalized configuration into the application container.
@@ -25,13 +26,14 @@ export class ConfigModule {
25
26
  * ```
26
27
  */
27
28
  static forRoot(options) {
29
+ const loadOptions = snapshotConfigModuleOptions(options);
28
30
  class ConfigModuleImpl extends ConfigModule {}
29
31
  defineModuleMetadata(ConfigModuleImpl, {
30
- global: options?.isGlobal ?? true,
32
+ global: loadOptions.isGlobal ?? true,
31
33
  exports: [ConfigService],
32
34
  providers: [{
33
35
  provide: ConfigService,
34
- useFactory: () => createConfigServiceFromSnapshot(loadConfig(options ?? {}))
36
+ useFactory: () => createConfigServiceFromSnapshot(loadConfig(loadOptions))
35
37
  }]
36
38
  });
37
39
  return ConfigModuleImpl;
@@ -0,0 +1,16 @@
1
+ import type { ConfigLoadOptions, ConfigModuleOptions } from './types.js';
2
+ /**
3
+ * Creates a detached snapshot of config module registration options.
4
+ *
5
+ * @param options Caller-owned module options captured at registration time.
6
+ * @returns Options that cannot observe later caller mutations of config dictionaries.
7
+ */
8
+ export declare function snapshotConfigModuleOptions(options?: ConfigModuleOptions): ConfigModuleOptions;
9
+ /**
10
+ * Creates a detached snapshot of config load and reload options.
11
+ *
12
+ * @param options Caller-owned load options captured by loaders or reload modules.
13
+ * @returns Options that preserve registration-time config dictionary inputs.
14
+ */
15
+ export declare function snapshotConfigLoadOptions(options?: ConfigLoadOptions): ConfigLoadOptions;
16
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,51 @@
1
+ import { cloneConfigDictionary } from './clone.js';
2
+ function snapshotConfigDictionary(value) {
3
+ return value === undefined ? undefined : cloneConfigDictionary(value);
4
+ }
5
+ function snapshotProcessEnv(processEnv) {
6
+ if (processEnv === undefined) {
7
+ return undefined;
8
+ }
9
+ const snapshot = {};
10
+ for (const [key, value] of Object.entries(processEnv)) {
11
+ if (value !== undefined) {
12
+ snapshot[key] = value;
13
+ }
14
+ }
15
+ return Object.freeze(snapshot);
16
+ }
17
+
18
+ /**
19
+ * Creates a detached snapshot of config module registration options.
20
+ *
21
+ * @param options Caller-owned module options captured at registration time.
22
+ * @returns Options that cannot observe later caller mutations of config dictionaries.
23
+ */
24
+ export function snapshotConfigModuleOptions(options) {
25
+ if (options === undefined) {
26
+ return {};
27
+ }
28
+ return Object.freeze({
29
+ ...options,
30
+ defaults: snapshotConfigDictionary(options.defaults),
31
+ processEnv: snapshotProcessEnv(options.processEnv)
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Creates a detached snapshot of config load and reload options.
37
+ *
38
+ * @param options Caller-owned load options captured by loaders or reload modules.
39
+ * @returns Options that preserve registration-time config dictionary inputs.
40
+ */
41
+ export function snapshotConfigLoadOptions(options) {
42
+ if (options === undefined) {
43
+ return {};
44
+ }
45
+ return Object.freeze({
46
+ ...options,
47
+ defaults: snapshotConfigDictionary(options.defaults),
48
+ processEnv: snapshotProcessEnv(options.processEnv),
49
+ runtimeOverrides: snapshotConfigDictionary(options.runtimeOverrides)
50
+ });
51
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"reload-module.d.ts","sourceRoot":"","sources":["../src/reload-module.ts"],"names":[],"mappings":"AAKA,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;;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"}
@@ -8,6 +8,7 @@ import { Inject } 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';
11
+ import { snapshotConfigLoadOptions } from './options.js';
11
12
  import { ConfigService, replaceConfigServiceSnapshotUnchecked } from './service.js';
12
13
  const CONFIG_RELOAD_OPTIONS = Symbol('fluo.config.reload-options');
13
14
 
@@ -110,7 +111,7 @@ class ConfigReloadManager {
110
111
  export { _ConfigReloadManager as ConfigReloadManager };
111
112
  export class ConfigReloadModule {
112
113
  static forRoot(options) {
113
- const loadOptions = options ?? {};
114
+ const loadOptions = snapshotConfigLoadOptions(options);
114
115
  class ConfigReloadModuleImpl extends ConfigReloadModule {}
115
116
  defineModuleMetadata(ConfigReloadModuleImpl, {
116
117
  exports: [CONFIG_RELOADER],
package/dist/types.d.ts CHANGED
@@ -1,7 +1,15 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
1
2
  /**
2
3
  * Plain JSON-like object used as the normalized configuration snapshot shape.
3
4
  */
4
5
  export type ConfigDictionary = Record<string, unknown>;
6
+ /**
7
+ * Standard Schema v1-compatible config validator accepted by `@fluojs/config` loaders.
8
+ *
9
+ * @typeParam Input Raw merged config shape consumed by the schema validator.
10
+ * @typeParam Output Normalized config shape produced by the schema validator.
11
+ */
12
+ export type ConfigSchema<Input = unknown, Output extends ConfigDictionary = ConfigDictionary> = StandardSchemaV1<Input, Output>;
5
13
  /**
6
14
  * Nested dot-path key helper.
7
15
  * Produces "a" | "a.b" | "a.b.c" keys from a Record type.
@@ -20,7 +28,7 @@ export interface ConfigModuleOptions {
20
28
  envFile?: string;
21
29
  envFilePath?: string;
22
30
  processEnv?: NodeJS.ProcessEnv;
23
- validate?: (raw: ConfigDictionary) => ConfigDictionary;
31
+ schema?: ConfigSchema;
24
32
  defaults?: ConfigDictionary;
25
33
  /** Supply a custom file parser (e.g. for YAML or TOML). Receives raw file content,
26
34
  * returns a flat key-value record. Defaults to dotenv parsing. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEvD;;;GAGG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,EAAE,MAAM,SAAS,MAAM,GAAG,EAAE,IAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnF;KACG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,GAClB,GAAG,MAAM,GAAG,CAAC,EAAE,GACf,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC;CACrC,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,GACnB,KAAK,CAAC;AAEV;;GAEG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,MAAM,CAAC,GACzD,CAAC,CAAC,CAAC,CAAC,GACJ,CAAC,SAAS,GAAG,MAAM,IAAI,IAAI,MAAM,IAAI,EAAE,GACrC,IAAI,SAAS,MAAM,CAAC,GAClB,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,GACvB,KAAK,GACP,KAAK,CAAC;AAEZ;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;IACvD,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;uEACmE;IACnE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAEpG;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAE7F;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,WAAW,IAAI,IAAI,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,gBAAgB,CAAC;IAC5B,MAAM,IAAI,gBAAgB,CAAC;IAC3B,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,wBAAwB,CAAC;IACpE,cAAc,CAAC,QAAQ,EAAE,yBAAyB,GAAG,wBAAwB,CAAC;IAC9E,KAAK,IAAI,IAAI,CAAC;CACf"}
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,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAEpG;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAE7F;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,WAAW,IAAI,IAAI,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,gBAAgB,CAAC;IAC5B,MAAM,IAAI,gBAAgB,CAAC;IAC3B,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,wBAAwB,CAAC;IACpE,cAAc,CAAC,QAAQ,EAAE,yBAAyB,GAAG,wBAAwB,CAAC;IAC9E,KAAK,IAAI,IAAI,CAAC;CACf"}
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "environment",
9
9
  "typed-config"
10
10
  ],
11
- "version": "1.0.0-beta.3",
11
+ "version": "1.0.0-beta.5",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -35,9 +35,10 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
+ "@standard-schema/spec": "^1.1.0",
38
39
  "dotenv": "^16.0.0",
39
40
  "dotenv-expand": "^11.0.0",
40
- "@fluojs/core": "^1.0.0-beta.2"
41
+ "@fluojs/core": "^1.0.0-beta.3"
41
42
  },
42
43
  "devDependencies": {
43
44
  "vitest": "^3.2.4"