@fluojs/config
Advanced tools
+6
-0
@@ -0,2 +1,8 @@ | ||
| /** | ||
| * Clone config dictionary. | ||
| * | ||
| * @param value The value. | ||
| * @returns The clone config dictionary result. | ||
| */ | ||
| export declare function cloneConfigDictionary<T>(value: T): T; | ||
| //# sourceMappingURL=clone.d.ts.map |
@@ -1,1 +0,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"} | ||
| {"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"} |
+7
-0
| import { fallbackClone } from '@fluojs/core/internal'; | ||
| /** | ||
| * Clone config dictionary. | ||
| * | ||
| * @param value The value. | ||
| * @returns The clone config dictionary result. | ||
| */ | ||
| export function cloneConfigDictionary(value) { | ||
@@ -3,0 +10,0 @@ try { |
+2
-2
@@ -5,3 +5,3 @@ import type { ConfigDictionary, ConfigLoadOptions, ConfigReloader } from './types.js'; | ||
| * | ||
| * @param options Configuration loading options, including optional watch mode and validation hooks. | ||
| * @param options Configuration loading options, including optional watch mode and a synchronous Standard Schema validator. | ||
| * @returns A reloader that exposes the current snapshot, manual reload, subscriptions, and cleanup. | ||
@@ -29,3 +29,3 @@ * @throws {FluoError} When the initial config load or validation fails. | ||
| * | ||
| * @param options Configuration loading options for source precedence, parsing, and validation. | ||
| * @param options Configuration loading options for source precedence, parsing, and synchronous schema validation. | ||
| * @returns A detached normalized configuration dictionary for the current load. | ||
@@ -32,0 +32,0 @@ * @throws {FluoError} When validation throws or the config cannot be normalized. |
@@ -1,1 +0,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"} | ||
| {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAEjB,cAAc,EAKf,MAAM,YAAY,CAAC;AA0XpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CA8B/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAEvE"} |
+79
-8
@@ -35,3 +35,12 @@ import { existsSync, readFileSync, watch } from 'node:fs'; | ||
| } | ||
| function rejectLegacyValidateOption(options) { | ||
| if ('validate' in options) { | ||
| throw new FluoError('Invalid configuration.', { | ||
| code: 'INVALID_CONFIG', | ||
| cause: new Error('The legacy `validate` option was removed. Use `schema` with a synchronous Standard Schema validator instead.') | ||
| }); | ||
| } | ||
| } | ||
| function normalizeLoadOptions(options) { | ||
| rejectLegacyValidateOption(options); | ||
| const cwd = options.cwd ?? process.cwd(); | ||
@@ -49,3 +58,3 @@ const envFile = options.envFilePath ?? options.envFile ?? join(cwd, '.env'); | ||
| safeProcessEnv, | ||
| validate: options.validate | ||
| schema: options.schema | ||
| }; | ||
@@ -92,10 +101,72 @@ } | ||
| } | ||
| function isPromiseLike(value) { | ||
| return (typeof value === 'object' || typeof value === 'function') && value !== null && 'then' in value && typeof value.then === 'function'; | ||
| } | ||
| function isConfigSchemaIssue(value) { | ||
| return typeof value === 'object' && value !== null && 'message' in value && typeof value.message === 'string'; | ||
| } | ||
| function isConfigSchemaFailureResult(value) { | ||
| if (typeof value !== 'object' || value === null || !('issues' in value)) { | ||
| return false; | ||
| } | ||
| return Array.isArray(value.issues) && value.issues.every(isConfigSchemaIssue); | ||
| } | ||
| function isConfigSchemaSuccessResult(value) { | ||
| return typeof value === 'object' && value !== null && 'value' in value && isPlainObject(value.value); | ||
| } | ||
| function isConfigSchemaPathKeySegment(value) { | ||
| if (typeof value !== 'object' || value === null || !('key' in value)) { | ||
| return false; | ||
| } | ||
| return typeof value.key === 'string' || typeof value.key === 'number' || typeof value.key === 'symbol'; | ||
| } | ||
| function formatConfigSchemaPathSegment(segment) { | ||
| if (typeof segment === 'string' || typeof segment === 'number') { | ||
| return String(segment); | ||
| } | ||
| if (isConfigSchemaPathKeySegment(segment)) { | ||
| return String(segment.key); | ||
| } | ||
| return undefined; | ||
| } | ||
| function formatConfigSchemaIssue(issue) { | ||
| const path = issue.path?.map(formatConfigSchemaPathSegment).filter(segment => segment !== undefined).join('.'); | ||
| return path && path.length > 0 ? `${path}: ${issue.message}` : issue.message; | ||
| } | ||
| function createInvalidConfigError(cause, issues) { | ||
| return new FluoError('Invalid configuration.', { | ||
| code: 'INVALID_CONFIG', | ||
| cause, | ||
| meta: issues ? { | ||
| issues: issues.map(formatConfigSchemaIssue) | ||
| } : undefined | ||
| }); | ||
| } | ||
| function isInvalidConfigError(error) { | ||
| return error instanceof FluoError && error.code === 'INVALID_CONFIG'; | ||
| } | ||
| function readConfigSchemaResult(result) { | ||
| if (isConfigSchemaFailureResult(result)) { | ||
| throw createInvalidConfigError(new Error('Standard Schema config validation failed.'), result.issues); | ||
| } | ||
| if (!isConfigSchemaSuccessResult(result)) { | ||
| throw createInvalidConfigError(new Error('Standard Schema config validator returned a malformed result.')); | ||
| } | ||
| return result.value; | ||
| } | ||
| function validateConfig(options, merged) { | ||
| if (!options.schema) { | ||
| return merged; | ||
| } | ||
| try { | ||
| return options.validate ? options.validate(merged) : merged; | ||
| const result = options.schema['~standard'].validate(merged); | ||
| if (isPromiseLike(result)) { | ||
| throw new Error('Config schemas must validate synchronously. Async Standard Schema validation is not supported by the synchronous config API.'); | ||
| } | ||
| return readConfigSchemaResult(result); | ||
| } catch (error) { | ||
| throw new FluoError('Invalid configuration.', { | ||
| code: 'INVALID_CONFIG', | ||
| cause: error | ||
| }); | ||
| if (isInvalidConfigError(error)) { | ||
| throw error; | ||
| } | ||
| throw createInvalidConfigError(error); | ||
| } | ||
@@ -186,3 +257,3 @@ } | ||
| * | ||
| * @param options Configuration loading options, including optional watch mode and validation hooks. | ||
| * @param options Configuration loading options, including optional watch mode and a synchronous Standard Schema validator. | ||
| * @returns A reloader that exposes the current snapshot, manual reload, subscriptions, and cleanup. | ||
@@ -239,3 +310,3 @@ * @throws {FluoError} When the initial config load or validation fails. | ||
| * | ||
| * @param options Configuration loading options for source precedence, parsing, and validation. | ||
| * @param options Configuration loading options for source precedence, parsing, and synchronous schema validation. | ||
| * @returns A detached normalized configuration dictionary for the current load. | ||
@@ -242,0 +313,0 @@ * @throws {FluoError} When validation throws or the config cannot be normalized. |
+9
-1
@@ -0,1 +1,2 @@ | ||
| import type { StandardSchemaV1 } from '@standard-schema/spec'; | ||
| /** | ||
@@ -6,2 +7,9 @@ * Plain JSON-like object used as the normalized configuration snapshot shape. | ||
| /** | ||
| * Standard Schema v1-compatible config validator accepted by `@fluojs/config` loaders. | ||
| * | ||
| * @typeParam Input Raw merged config shape consumed by the schema validator. | ||
| * @typeParam Output Normalized config shape produced by the schema validator. | ||
| */ | ||
| export type ConfigSchema<Input = unknown, Output extends ConfigDictionary = ConfigDictionary> = StandardSchemaV1<Input, Output>; | ||
| /** | ||
| * Nested dot-path key helper. | ||
@@ -24,3 +32,3 @@ * Produces "a" | "a.b" | "a.b.c" keys from a Record type. | ||
| processEnv?: NodeJS.ProcessEnv; | ||
| validate?: (raw: ConfigDictionary) => ConfigDictionary; | ||
| schema?: ConfigSchema; | ||
| defaults?: ConfigDictionary; | ||
@@ -27,0 +35,0 @@ /** Supply a custom file parser (e.g. for YAML or TOML). Receives raw file content, |
@@ -1,1 +0,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"} | ||
| {"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"} |
+2
-1
@@ -11,3 +11,3 @@ { | ||
| ], | ||
| "version": "1.0.0-beta.3", | ||
| "version": "1.0.0-beta.4", | ||
| "private": false, | ||
@@ -39,2 +39,3 @@ "license": "MIT", | ||
| "dependencies": { | ||
| "@standard-schema/spec": "^1.1.0", | ||
| "dotenv": "^16.0.0", | ||
@@ -41,0 +42,0 @@ "dotenv-expand": "^11.0.0", |
+11
-6
@@ -35,3 +35,9 @@ # @fluojs/config | ||
| import { Module } from '@fluojs/core'; | ||
| import { z } from 'zod'; | ||
| const EnvSchema = z.object({ | ||
| DATABASE_URL: z.string().url(), | ||
| PORT: z.coerce.number().default(3000), | ||
| }); | ||
| @Module({ | ||
@@ -45,6 +51,3 @@ imports: [ | ||
| defaults: { PORT: '3000' }, | ||
| validate: (config) => { | ||
| if (!config.DATABASE_URL) throw new Error('DATABASE_URL이 필요합니다'); | ||
| return config; | ||
| }, | ||
| schema: EnvSchema, | ||
| }), | ||
@@ -83,4 +86,6 @@ ], | ||
| `validate` 함수는 모든 소스가 합쳐진 뒤 실행되며, 에러를 던지면 부트스트랩이 즉시 중단됩니다. | ||
| `schema` 옵션은 Zod, Valibot, ArkType 같은 동기식 [Standard Schema](https://standardschema.dev/schema) 호환 validator를 받습니다. 스키마는 모든 소스가 합쳐진 뒤 실행되고, 검증된 `value`가 최종 config snapshot이 됩니다. schema issue가 보고되면 bootstrap/load/reload는 `INVALID_CONFIG`로 실패합니다. | ||
| `@fluojs/config`의 load와 reload API는 동기식입니다. 비동기 Standard Schema 결과는 `INVALID_CONFIG`로 거부되므로 config 검증에는 동기 스키마를 사용하세요. | ||
| ### 런타임 접근과 리로드 비용 모델 | ||
@@ -109,3 +114,3 @@ | ||
| - `@fluojs/runtime`: 부트스트랩 중 `loadConfig()`를 호출합니다. | ||
| - `@fluojs/validation`: `validate` 함수 안에서 스키마 기반 검증을 조합할 수 있습니다. | ||
| - Standard Schema validator: Zod, Valibot, ArkType 및 호환 schema 라이브러리를 `schema` 옵션으로 전달할 수 있습니다. | ||
@@ -112,0 +117,0 @@ ## 예제 소스 |
+11
-6
@@ -38,3 +38,9 @@ # @fluojs/config | ||
| import { ConfigModule } from '@fluojs/config'; | ||
| import { z } from 'zod'; | ||
| const EnvSchema = z.object({ | ||
| DATABASE_URL: z.string().url(), | ||
| PORT: z.coerce.number().default(3000), | ||
| }); | ||
| @Module({ | ||
@@ -48,6 +54,3 @@ imports: [ | ||
| defaults: { PORT: '3000' }, | ||
| validate: (config) => { | ||
| if (!config.DATABASE_URL) throw new Error('DATABASE_URL is required'); | ||
| return config; | ||
| }, | ||
| schema: EnvSchema, | ||
| }), | ||
@@ -88,4 +91,6 @@ ], | ||
| ### Validation | ||
| The `validate` function runs after all sources are merged but before the application starts. If it throws, the application bootstrap fails immediately. | ||
| 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`. | ||
| `@fluojs/config` keeps loading and reload APIs synchronous. Async Standard Schema results are rejected with `INVALID_CONFIG`; use a synchronous schema for config validation. | ||
| ### Runtime Access and Reload Cost Model | ||
@@ -113,3 +118,3 @@ `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. | ||
| - **`@fluojs/runtime`**: Calls `loadConfig` internally during application bootstrap. | ||
| - **`@fluojs/validation`**: Can be used within the `validate` function for schema-based validation. | ||
| - **Standard Schema validators**: Zod, Valibot, ArkType, and other compatible schema libraries can be passed through the `schema` option. | ||
@@ -116,0 +121,0 @@ ## Example Sources |
51395
9.92%820
12.48%122
4.27%4
33.33%+ Added
+ Added