From e0f81e9f4d001665c5bc6ef604f0bc2aeac0e383 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 28 Jan 2026 15:06:08 +0200 Subject: [PATCH 1/5] Implement createConfig --- README.md | 44 ++++++++++++++++++ src/core.test.ts | 116 ++++++++++++++++++++++++++++++++++++++++++++++- src/core.ts | 55 ++++++++++++++++++++++ src/index.ts | 9 +++- src/types.ts | 14 ++++++ 5 files changed, 235 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 44547df..d314820 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Works with Zod, Valibot, ArkType, and other Standard Schema-compatible validatio - 🔌 **Standard Schema compliant** - Works with any compatible validation library - 🌐 **Runtime agnostic** - Runs anywhere (Node, Bun, Deno, browsers) - 🏗️ **Structured configuration** - Supports nested config objects +- 🧮 **Computed values** - Derive values from parsed config with full type inference - 🚦 **Environment detection** - `isProduction`, `isTest`, `isDevelopment` flags - 📜 **Detailed error reporting** - See all validation failures at once - 🚀 **Lightweight** - Single dependency (type-fest), zero runtime overhead @@ -132,6 +133,40 @@ type Config = InferEnv; // { apiKey: string; db: { host: string } } ``` +### Computed Values + +Use `createConfig` to derive computed values from your parsed configuration with full type inference: + +```typescript +import { createConfig, envvar } from 'envase'; +import { z } from 'zod'; + +const config = createConfig(process.env, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + name: envvar('DB_NAME', z.string()), + }, + api: { + key: envvar('API_KEY', z.string()), + }, + }, + computed: { + dbConnectionString: (raw) => + `postgres://${raw.db.host}:${raw.db.port}/${raw.db.name}`, + apiKeyPrefix: (raw) => raw.api.key.slice(0, 8), + }, +}); + +// config.db.host -> string +// config.db.port -> number +// config.dbConnectionString -> string +// config.apiKeyPrefix -> string +``` + +The `raw` parameter in computed functions is fully typed based on your schema, providing autocomplete and type checking. Computed values are calculated after schema validation, so you always work with parsed values (e.g., `port` is a `number`, not a string). + ## CLI Automatically generate and validate markdown documentation from your environment variable schemas. @@ -277,6 +312,15 @@ This helps pair the raw env name with the shape you expect it to conform to. Validates envvars against the schema and returns a typed configuration object. +### `createConfig` + +`createConfig(env: Record, options: { schema: T, computed?: C })` + +Validates envvars and optionally computes derived values. Returns a merged object containing both the parsed config and computed values. + +- `schema` - Environment variable schema (same format as `parseEnv`) +- `computed` - Optional object where each key is a function receiving the parsed config and returning a derived value + ### `detectNodeEnv` `detectNodeEnv(env: Record)` diff --git a/src/core.test.ts b/src/core.test.ts index 3afffdb..05f8870 100644 --- a/src/core.test.ts +++ b/src/core.test.ts @@ -1,7 +1,7 @@ import * as v from 'valibot'; import { describe, expect, it } from 'vitest'; import { z } from 'zod'; -import { detectNodeEnv, envvar, parseEnv } from './core.ts'; +import { createConfig, detectNodeEnv, envvar, parseEnv } from './core.ts'; describe('core', () => { describe('detectNodeEnv', () => { @@ -217,4 +217,118 @@ describe('core', () => { }); }); }); + + describe('createConfig', () => { + const mockEnv = { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_NAME: 'mydb', + API_KEY: 'secret123', + }; + + it('parses config without computed values', () => { + const config = createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + }, + }, + }); + + expect(config.db.host).toBe('localhost'); + expect(config.db.port).toBe(5432); + }); + + it('computes derived values from raw config', () => { + const config = createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + name: envvar('DB_NAME', z.string()), + }, + api: { + key: envvar('API_KEY', z.string()), + }, + }, + computed: { + dbConnectionString: (raw) => + `postgres://${raw.db.host}:${raw.db.port}/${raw.db.name}`, + apiKeyPrefix: (raw) => raw.api.key.slice(0, 8), + }, + }); + + expect(config.db.host).toBe('localhost'); + expect(config.db.port).toBe(5432); + expect(config.db.name).toBe('mydb'); + expect(config.api.key).toBe('secret123'); + expect(config.dbConnectionString).toBe('postgres://localhost:5432/mydb'); + expect(config.apiKeyPrefix).toBe('secret12'); + }); + + it('receives parsed values in computed functions (not raw strings)', () => { + const config = createConfig(mockEnv, { + schema: { + port: envvar('DB_PORT', z.coerce.number()), + }, + computed: { + portPlusTen: (raw) => raw.port + 10, + }, + }); + + expect(config.port).toBe(5432); + expect(config.portPlusTen).toBe(5442); + }); + + it('throws EnvaseError if schema validation fails before computing', () => { + expect(() => + createConfig(mockEnv, { + schema: { + missing: envvar('MISSING_VAR', z.string()), + }, + computed: { + derived: (raw) => raw.missing.toUpperCase(), + }, + }), + ).toThrow(); + }); + + it('works with empty computed object', () => { + const config = createConfig(mockEnv, { + schema: { + host: envvar('DB_HOST', z.string()), + }, + computed: {}, + }); + + expect(config.host).toBe('localhost'); + }); + + it('infers types correctly for raw parameter and return values', () => { + const config = createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + }, + }, + computed: { + connectionString: (raw) => `${raw.db.host}:${raw.db.port}`, + portPlusTen: (raw) => raw.db.port + 10, + }, + }); + + // These type assertions verify compile-time inference + const _host: string = config.db.host; + const _port: number = config.db.port; + const _connStr: string = config.connectionString; + const _portPlusTen: number = config.portPlusTen; + + expect(_host).toBe('localhost'); + expect(_port).toBe(5432); + expect(_connStr).toBe('localhost:5432'); + expect(_portPlusTen).toBe(5442); + }); + }); }); diff --git a/src/core.ts b/src/core.ts index c0af998..6fc3400 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,9 +1,11 @@ import { EnvaseError } from './errors/envase-error.ts'; import type { StandardSchemaV1 } from './standard-schema.ts'; import type { + ComputedSchema, EnvSchema, EnvvarEntry, EnvvarValidationIssue, + InferConfig, InferEnv, NodeEnvInfo, } from './types.ts'; @@ -76,3 +78,56 @@ export const parseEnv = ( return config; }; + +// Overload: without computed +export function createConfig( + env: Record, + options: { + schema: TSchema; + computed?: undefined; + }, +): InferEnv; + +// Overload: with computed +export function createConfig< + TSchema extends EnvSchema, + const TComputed extends ComputedSchema>, +>( + env: Record, + options: { + schema: TSchema; + computed: TComputed; + }, +): InferConfig, TComputed>; + +// Implementation +export function createConfig< + TSchema extends EnvSchema, + TComputed extends ComputedSchema>, +>( + env: Record, + options: { + schema: TSchema; + computed?: TComputed; + }, + // biome-ignore lint/suspicious/noExplicitAny: Required for overload implementation +): any { + // Parse raw config using existing parseEnv + const rawConfig = parseEnv(env, options.schema); + + // If no computed values, return raw config + if (!options.computed) { + return rawConfig; + } + + // Compute derived values + const computedValues = Object.fromEntries( + Object.entries(options.computed).map(([key, resolver]) => [ + key, + resolver(rawConfig), + ]), + ); + + // Merge raw config with computed values + return Object.assign({}, rawConfig, computedValues); +} diff --git a/src/index.ts b/src/index.ts index c7b14fc..26ffff7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,8 @@ -export { detectNodeEnv, envvar, parseEnv } from './core.ts'; +export { createConfig, detectNodeEnv, envvar, parseEnv } from './core.ts'; export { EnvaseError } from './errors/envase-error.ts'; -export type { InferEnv } from './types.ts'; +export type { + ComputedSchema, + InferComputed, + InferConfig, + InferEnv, +} from './types.ts'; diff --git a/src/types.ts b/src/types.ts index c812faf..e8eed40 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,3 +28,17 @@ export type EnvvarValidationIssue = { value?: string; messages: string[]; }; + +// Schema for computed values - keys map to resolver functions +export type ComputedSchema = Record unknown>; + +// Infer output types from computed schema +export type InferComputed = { + // biome-ignore lint/suspicious/noExplicitAny: Required for type inference + [K in keyof T]: T[K] extends (raw: any) => infer R ? R : never; +}; + +// Combined result type (raw config merged with computed values) +export type InferConfig = SimplifyDeep< + TRaw & InferComputed +>; From 276daaef63e8174f8a5d99845795c1443f3e2718 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 28 Jan 2026 15:12:07 +0200 Subject: [PATCH 2/5] fix docs --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d314820..26249d1 100644 --- a/README.md +++ b/README.md @@ -314,12 +314,13 @@ Validates envvars against the schema and returns a typed configuration object. ### `createConfig` -`createConfig(env: Record, options: { schema: T, computed?: C })` +`createConfig(env, options)` -Validates envvars and optionally computes derived values. Returns a merged object containing both the parsed config and computed values. +Validates envvars and optionally computes derived values. Returns a merged object containing both the parsed config and computed values. All types are inferred from the schema and computed functions. -- `schema` - Environment variable schema (same format as `parseEnv`) -- `computed` - Optional object where each key is a function receiving the parsed config and returning a derived value +- `env` - Environment variables object (e.g., `process.env`) +- `options.schema` - Environment variable schema (same format as `parseEnv`) +- `options.computed` - Optional object where each key is a function receiving the parsed config and returning a derived value ### `detectNodeEnv` From 7ad15395dd155c9d8b18e0ffa31f12f89c9e06cf Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 28 Jan 2026 15:25:37 +0200 Subject: [PATCH 3/5] Add merging --- README.md | 61 ++++++++++++++++++++++++++++++ src/core.test.ts | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ src/core.ts | 64 +++++++++++++++++++++++++++---- src/types.ts | 38 +++++++++++++++---- 4 files changed, 246 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 26249d1..fef27ff 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,67 @@ const config = createConfig(process.env, { The `raw` parameter in computed functions is fully typed based on your schema, providing autocomplete and type checking. Computed values are calculated after schema validation, so you always work with parsed values (e.g., `port` is a `number`, not a string). +#### Nested Computed Values + +Computed values can be nested to merge with your schema structure: + +```typescript +import { createConfig, envvar } from 'envase'; +import { z } from 'zod'; + +const config = createConfig(process.env, { + schema: { + aws: { + accessKeyId: envvar('AWS_ACCESS_KEY_ID', z.string()), + secretAccessKey: envvar('AWS_SECRET_ACCESS_KEY', z.string()), + }, + }, + computed: { + aws: { + credentials: (raw) => ({ + accessKeyId: raw.aws.accessKeyId, + secretAccessKey: raw.aws.secretAccessKey, + }), + }, + }, +}); + +// Result type: +// { +// aws: { +// accessKeyId: string; +// secretAccessKey: string; +// credentials: { accessKeyId: string; secretAccessKey: string }; +// } +// } +``` + +You can also mix flat and nested computed values: + +```typescript +const config = createConfig(process.env, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + }, + }, + computed: { + // Flat at root level + dbUrl: (raw) => `${raw.db.host}:${raw.db.port}`, + // Nested under existing schema key + db: { + connectionString: (raw) => `postgres://${raw.db.host}:${raw.db.port}`, + }, + }, +}); + +// config.dbUrl -> string +// config.db.host -> string +// config.db.port -> number +// config.db.connectionString -> string +``` + ## CLI Automatically generate and validate markdown documentation from your environment variable schemas. diff --git a/src/core.test.ts b/src/core.test.ts index 05f8870..d87fc65 100644 --- a/src/core.test.ts +++ b/src/core.test.ts @@ -330,5 +330,103 @@ describe('core', () => { expect(_connStr).toBe('localhost:5432'); expect(_portPlusTen).toBe(5442); }); + + it('deep merges nested computed values with schema', () => { + const config = createConfig( + { + AWS_ACCESS_KEY_ID: 'AKIAIOSFODNN7EXAMPLE', + AWS_SECRET_ACCESS_KEY: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + }, + { + schema: { + aws: { + accessKeyId: envvar('AWS_ACCESS_KEY_ID', z.string()), + secretAccessKey: envvar('AWS_SECRET_ACCESS_KEY', z.string()), + }, + }, + computed: { + aws: { + credentials: (raw) => ({ + accessKeyId: raw.aws.accessKeyId, + secretAccessKey: raw.aws.secretAccessKey, + }), + }, + }, + }, + ); + + // Verify schema values are preserved + expect(config.aws.accessKeyId).toBe('AKIAIOSFODNN7EXAMPLE'); + expect(config.aws.secretAccessKey).toBe( + 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + ); + + // Verify computed value is merged in + expect(config.aws.credentials).toEqual({ + accessKeyId: 'AKIAIOSFODNN7EXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + }); + + // Type assertions + const _accessKeyId: string = config.aws.accessKeyId; + const _credentials: { + accessKeyId: string; + secretAccessKey: string; + } = config.aws.credentials; + + expect(_accessKeyId).toBeDefined(); + expect(_credentials).toBeDefined(); + }); + + it('supports multiple levels of nested computed values', () => { + const config = createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + name: envvar('DB_NAME', z.string()), + }, + }, + computed: { + db: { + connection: { + url: (raw) => + `postgres://${raw.db.host}:${raw.db.port}/${raw.db.name}`, + }, + }, + }, + }); + + expect(config.db.host).toBe('localhost'); + expect(config.db.port).toBe(5432); + expect(config.db.name).toBe('mydb'); + expect(config.db.connection.url).toBe('postgres://localhost:5432/mydb'); + }); + + it('allows mixing flat and nested computed values', () => { + const config = createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + }, + api: { + key: envvar('API_KEY', z.string()), + }, + }, + computed: { + // Flat computed value at root + dbUrl: (raw) => `${raw.db.host}:${raw.db.port}`, + // Nested computed value + api: { + keyPrefix: (raw) => raw.api.key.slice(0, 4), + }, + }, + }); + + expect(config.dbUrl).toBe('localhost:5432'); + expect(config.api.key).toBe('secret123'); + expect(config.api.keyPrefix).toBe('secr'); + }); }); }); diff --git a/src/core.ts b/src/core.ts index 6fc3400..40c5441 100644 --- a/src/core.ts +++ b/src/core.ts @@ -79,6 +79,56 @@ export const parseEnv = ( return config; }; +// Helper to check if value is a resolver function +const isResolver = (value: unknown): value is (raw: unknown) => unknown => + typeof value === 'function'; + +// Helper to process computed schema recursively +const processComputed = ( + computed: Record, + rawConfig: unknown, +): Record => { + return Object.fromEntries( + Object.entries(computed).map(([key, value]) => [ + key, + isResolver(value) + ? value(rawConfig) + : processComputed(value as Record, rawConfig), + ]), + ); +}; + +// Helper to deep merge two objects +const deepMerge = ( + target: Record, + source: Record, +): Record => { + const result = { ...target }; + + for (const key of Object.keys(source)) { + const sourceValue = source[key]; + const targetValue = result[key]; + + if ( + sourceValue && + typeof sourceValue === 'object' && + !Array.isArray(sourceValue) && + targetValue && + typeof targetValue === 'object' && + !Array.isArray(targetValue) + ) { + result[key] = deepMerge( + targetValue as Record, + sourceValue as Record, + ); + } else { + result[key] = sourceValue; + } + } + + return result; +}; + // Overload: without computed export function createConfig( env: Record, @@ -120,14 +170,12 @@ export function createConfig< return rawConfig; } - // Compute derived values - const computedValues = Object.fromEntries( - Object.entries(options.computed).map(([key, resolver]) => [ - key, - resolver(rawConfig), - ]), + // Compute derived values (handles nested structures) + const computedValues = processComputed( + options.computed as Record, + rawConfig, ); - // Merge raw config with computed values - return Object.assign({}, rawConfig, computedValues); + // Deep merge raw config with computed values + return deepMerge(rawConfig as Record, computedValues); } diff --git a/src/types.ts b/src/types.ts index e8eed40..1f64397 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,16 +29,40 @@ export type EnvvarValidationIssue = { messages: string[]; }; -// Schema for computed values - keys map to resolver functions -export type ComputedSchema = Record unknown>; +// Resolver function type for computed values +// biome-ignore lint/suspicious/noExplicitAny: Required for type inference +type ComputedResolver = (raw: any) => unknown; -// Infer output types from computed schema +// Schema for computed values - can be nested objects or resolver functions +export type ComputedSchema = { + [key: string]: ((raw: TRaw) => unknown) | ComputedSchema; +}; + +// Infer output types from computed schema (handles nested structures) export type InferComputed = { - // biome-ignore lint/suspicious/noExplicitAny: Required for type inference - [K in keyof T]: T[K] extends (raw: any) => infer R ? R : never; + [K in keyof T]: T[K] extends ComputedResolver + ? ReturnType + : T[K] extends object + ? InferComputed + : never; +}; + +// Deep merge two types (TComputed values override TRaw where keys overlap) +type DeepMerge = { + [K in keyof TRaw | keyof TComputed]: K extends keyof TComputed + ? K extends keyof TRaw + ? TRaw[K] extends object + ? TComputed[K] extends object + ? DeepMerge + : TComputed[K] + : TComputed[K] + : TComputed[K] + : K extends keyof TRaw + ? TRaw[K] + : never; }; -// Combined result type (raw config merged with computed values) +// Combined result type (raw config deep merged with computed values) export type InferConfig = SimplifyDeep< - TRaw & InferComputed + DeepMerge> >; From b4071808ce26bccee55ebd9429b53bc67fb5913d Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 28 Jan 2026 15:48:54 +0200 Subject: [PATCH 4/5] Add type tests --- package-lock.json | 324 +++++++++++++++++++++++----------------------- package.json | 14 +- src/core.test.ts | 159 ++++++++++++++++++++++- src/types.ts | 19 +-- vitest.config.ts | 3 + 5 files changed, 332 insertions(+), 187 deletions(-) diff --git a/package-lock.json b/package-lock.json index 768a8ec..c6b1865 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "cac": "^6.7.14", "diff": "^8.0.3", - "type-fest": "^5.4.1" + "type-fest": "^5.4.2" }, "bin": { "envase": "dist/cli/main.js" @@ -20,12 +20,12 @@ "@biomejs/biome": "^2.1.3", "@lokalise/tsconfig": "^2.0.0", "@types/node": "^25.0.9", - "@vitest/coverage-v8": "^4.0.17", - "rimraf": "^6.0.1", - "typescript": "^5.9.2", + "@vitest/coverage-v8": "^4.0.18", + "rimraf": "^6.1.2", + "typescript": "^5.9.3", "valibot": "^1.0.0", - "vitest": "^4.0.17", - "zod": "^4.0.15" + "vitest": "^4.0.18", + "zod": "^4.3.6" } }, "node_modules/@babel/helper-string-parser": { @@ -752,9 +752,9 @@ "license": "Apache-2.0" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.3.tgz", - "integrity": "sha512-qyX8+93kK/7R5BEXPC2PjUt0+fS/VO2BVHjEHyIEWiYn88rcRBHmdLgoJjktBltgAf+NY7RfCGB1SoyKS/p9kg==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", "cpu": [ "arm" ], @@ -766,9 +766,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.3.tgz", - "integrity": "sha512-6sHrL42bjt5dHQzJ12Q4vMKfN+kUnZ0atHHnv4V0Wd9JMTk7FDzSY35+7qbz3ypQYMBPANbpGK7JpnWNnhGt8g==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", "cpu": [ "arm64" ], @@ -780,9 +780,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.3.tgz", - "integrity": "sha512-1ht2SpGIjEl2igJ9AbNpPIKzb1B5goXOcmtD0RFxnwNuMxqkR6AUaaErZz+4o+FKmzxcSNBOLrzsICZVNYa1Rw==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", "cpu": [ "arm64" ], @@ -794,9 +794,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.3.tgz", - "integrity": "sha512-FYZ4iVunXxtT+CZqQoPVwPhH7549e/Gy7PIRRtq4t5f/vt54pX6eG9ebttRH6QSH7r/zxAFA4EZGlQ0h0FvXiA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", "cpu": [ "x64" ], @@ -808,9 +808,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.3.tgz", - "integrity": "sha512-M/mwDCJ4wLsIgyxv2Lj7Len+UMHd4zAXu4GQ2UaCdksStglWhP61U3uowkaYBQBhVoNpwx5Hputo8eSqM7K82Q==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", "cpu": [ "arm64" ], @@ -822,9 +822,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.3.tgz", - "integrity": "sha512-5jZT2c7jBCrMegKYTYTpni8mg8y3uY8gzeq2ndFOANwNuC/xJbVAoGKR9LhMDA0H3nIhvaqUoBEuJoICBudFrA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", "cpu": [ "x64" ], @@ -836,9 +836,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.3.tgz", - "integrity": "sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", "cpu": [ "arm" ], @@ -850,9 +850,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.3.tgz", - "integrity": "sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", "cpu": [ "arm" ], @@ -864,9 +864,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.3.tgz", - "integrity": "sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", "cpu": [ "arm64" ], @@ -878,9 +878,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.3.tgz", - "integrity": "sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", "cpu": [ "arm64" ], @@ -892,9 +892,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.3.tgz", - "integrity": "sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", "cpu": [ "loong64" ], @@ -906,9 +906,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.3.tgz", - "integrity": "sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", "cpu": [ "loong64" ], @@ -920,9 +920,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.3.tgz", - "integrity": "sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", "cpu": [ "ppc64" ], @@ -934,9 +934,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.3.tgz", - "integrity": "sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", "cpu": [ "ppc64" ], @@ -948,9 +948,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.3.tgz", - "integrity": "sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", "cpu": [ "riscv64" ], @@ -962,9 +962,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.3.tgz", - "integrity": "sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", "cpu": [ "riscv64" ], @@ -976,9 +976,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.3.tgz", - "integrity": "sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", "cpu": [ "s390x" ], @@ -990,9 +990,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.3.tgz", - "integrity": "sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", "cpu": [ "x64" ], @@ -1004,9 +1004,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.3.tgz", - "integrity": "sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", "cpu": [ "x64" ], @@ -1018,9 +1018,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.3.tgz", - "integrity": "sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", "cpu": [ "x64" ], @@ -1032,9 +1032,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.3.tgz", - "integrity": "sha512-vo54aXwjpTtsAnb3ca7Yxs9t2INZg7QdXN/7yaoG7nPGbOBXYXQY41Km+S1Ov26vzOAzLcAjmMdjyEqS1JkVhw==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", "cpu": [ "arm64" ], @@ -1046,9 +1046,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.3.tgz", - "integrity": "sha512-HI+PIVZ+m+9AgpnY3pt6rinUdRYrGHvmVdsNQ4odNqQ/eRF78DVpMR7mOq7nW06QxpczibwBmeQzB68wJ+4W4A==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", "cpu": [ "arm64" ], @@ -1060,9 +1060,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.3.tgz", - "integrity": "sha512-vRByotbdMo3Wdi+8oC2nVxtc3RkkFKrGaok+a62AT8lz/YBuQjaVYAS5Zcs3tPzW43Vsf9J0wehJbUY5xRSekA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", "cpu": [ "ia32" ], @@ -1074,9 +1074,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.3.tgz", - "integrity": "sha512-POZHq7UeuzMJljC5NjKi8vKMFN6/5EOqcX1yGntNLp7rUTpBAXQ1hW8kWPFxYLv07QMcNM75xqVLGPWQq6TKFA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", "cpu": [ "x64" ], @@ -1088,9 +1088,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.3.tgz", - "integrity": "sha512-aPFONczE4fUFKNXszdvnd2GqKEYQdV5oEsIbKPujJmWlCI9zEsv1Otig8RKK+X9bed9gFUN6LAeN4ZcNuu4zjg==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", "cpu": [ "x64" ], @@ -1145,14 +1145,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.17.tgz", - "integrity": "sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.17", + "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -1166,8 +1166,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.17", - "vitest": "4.0.17" + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1176,16 +1176,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz", - "integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.17", - "@vitest/utils": "4.0.17", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" }, @@ -1194,13 +1194,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz", - "integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.17", + "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1221,9 +1221,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz", - "integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { @@ -1234,13 +1234,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz", - "integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.17", + "@vitest/utils": "4.0.18", "pathe": "^2.0.3" }, "funding": { @@ -1248,13 +1248,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz", - "integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.17", + "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1263,9 +1263,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz", - "integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, "license": "MIT", "funding": { @@ -1273,13 +1273,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz", - "integrity": "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.17", + "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1725,9 +1725,9 @@ } }, "node_modules/rollup": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.3.tgz", - "integrity": "sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", "dev": true, "license": "MIT", "dependencies": { @@ -1741,31 +1741,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.3", - "@rollup/rollup-android-arm64": "4.55.3", - "@rollup/rollup-darwin-arm64": "4.55.3", - "@rollup/rollup-darwin-x64": "4.55.3", - "@rollup/rollup-freebsd-arm64": "4.55.3", - "@rollup/rollup-freebsd-x64": "4.55.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.3", - "@rollup/rollup-linux-arm-musleabihf": "4.55.3", - "@rollup/rollup-linux-arm64-gnu": "4.55.3", - "@rollup/rollup-linux-arm64-musl": "4.55.3", - "@rollup/rollup-linux-loong64-gnu": "4.55.3", - "@rollup/rollup-linux-loong64-musl": "4.55.3", - "@rollup/rollup-linux-ppc64-gnu": "4.55.3", - "@rollup/rollup-linux-ppc64-musl": "4.55.3", - "@rollup/rollup-linux-riscv64-gnu": "4.55.3", - "@rollup/rollup-linux-riscv64-musl": "4.55.3", - "@rollup/rollup-linux-s390x-gnu": "4.55.3", - "@rollup/rollup-linux-x64-gnu": "4.55.3", - "@rollup/rollup-linux-x64-musl": "4.55.3", - "@rollup/rollup-openbsd-x64": "4.55.3", - "@rollup/rollup-openharmony-arm64": "4.55.3", - "@rollup/rollup-win32-arm64-msvc": "4.55.3", - "@rollup/rollup-win32-ia32-msvc": "4.55.3", - "@rollup/rollup-win32-x64-gnu": "4.55.3", - "@rollup/rollup-win32-x64-msvc": "4.55.3", + "@rollup/rollup-android-arm-eabi": "4.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" } }, @@ -1883,9 +1883,9 @@ } }, "node_modules/type-fest": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.1.tgz", - "integrity": "sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.2.tgz", + "integrity": "sha512-FLEenlVYf7Zcd34ISMLo3ZzRE1gRjY1nMDTp+bQRBiPsaKyIW8K3Zr99ioHDUgA9OGuGGJPyYpNcffGmBhJfGg==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -2011,20 +2011,20 @@ } }, "node_modules/vitest": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz", - "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@vitest/expect": "4.0.17", - "@vitest/mocker": "4.0.17", - "@vitest/pretty-format": "4.0.17", - "@vitest/runner": "4.0.17", - "@vitest/snapshot": "4.0.17", - "@vitest/spy": "4.0.17", - "@vitest/utils": "4.0.17", + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", @@ -2052,10 +2052,10 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.17", - "@vitest/browser-preview": "4.0.17", - "@vitest/browser-webdriverio": "4.0.17", - "@vitest/ui": "4.0.17", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, @@ -2107,9 +2107,9 @@ } }, "node_modules/zod": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index d3c5a0f..440aa8e 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "build": "rimraf dist && tsc -p tsconfig.build.json", "lint": "biome check . && tsc", "lint:fix": "biome check --write .", - "test": "vitest --coverage", + "test": "vitest --typecheck --coverage", "prepublishOnly": "npm run build" }, "bin": { @@ -46,17 +46,17 @@ "dependencies": { "cac": "^6.7.14", "diff": "^8.0.3", - "type-fest": "^5.4.1" + "type-fest": "^5.4.2" }, "devDependencies": { "@biomejs/biome": "^2.1.3", "@lokalise/tsconfig": "^2.0.0", "@types/node": "^25.0.9", - "@vitest/coverage-v8": "^4.0.17", - "rimraf": "^6.0.1", - "typescript": "^5.9.2", + "@vitest/coverage-v8": "^4.0.18", + "rimraf": "^6.1.2", + "typescript": "^5.9.3", "valibot": "^1.0.0", - "vitest": "^4.0.17", - "zod": "^4.0.15" + "vitest": "^4.0.18", + "zod": "^4.3.6" } } diff --git a/src/core.test.ts b/src/core.test.ts index d87fc65..bc6d282 100644 --- a/src/core.test.ts +++ b/src/core.test.ts @@ -1,5 +1,5 @@ import * as v from 'valibot'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, expectTypeOf, it } from 'vitest'; import { z } from 'zod'; import { createConfig, detectNodeEnv, envvar, parseEnv } from './core.ts'; @@ -428,5 +428,162 @@ describe('core', () => { expect(config.api.key).toBe('secret123'); expect(config.api.keyPrefix).toBe('secr'); }); + + describe('type inference', () => { + it('infers correct types for config without computed values', () => { + const config = createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + }, + }, + }); + + expectTypeOf(config).toEqualTypeOf<{ + db: { host: string; port: number }; + }>(); + }); + + it('infers correct types for flat computed values', () => { + const config = createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + }, + }, + computed: { + url: (raw) => `${raw.db.host}:${raw.db.port}`, + portPlusTen: (raw) => raw.db.port + 10, + }, + }); + + expectTypeOf(config.db).toEqualTypeOf<{ host: string; port: number }>(); + expectTypeOf(config.url).toEqualTypeOf(); + expectTypeOf(config.portPlusTen).toEqualTypeOf(); + }); + + it('infers correct types for nested computed values merged with schema', () => { + const config = createConfig( + { + AWS_ACCESS_KEY_ID: 'test', + AWS_SECRET_ACCESS_KEY: 'test', + }, + { + schema: { + aws: { + accessKeyId: envvar('AWS_ACCESS_KEY_ID', z.string()), + secretAccessKey: envvar('AWS_SECRET_ACCESS_KEY', z.string()), + }, + }, + computed: { + aws: { + credentials: (raw) => ({ + key: raw.aws.accessKeyId, + secret: raw.aws.secretAccessKey, + }), + }, + }, + }, + ); + + expectTypeOf(config.aws.accessKeyId).toEqualTypeOf(); + expectTypeOf(config.aws.secretAccessKey).toEqualTypeOf(); + expectTypeOf(config.aws.credentials).toEqualTypeOf<{ + key: string; + secret: string; + }>(); + }); + + it('infers correct types for deeply nested computed values', () => { + const config = createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + }, + }, + computed: { + db: { + connection: { + url: (raw) => `postgres://${raw.db.host}:${raw.db.port}`, + isLocal: (raw) => raw.db.host === 'localhost', + }, + }, + }, + }); + + expectTypeOf(config.db.host).toEqualTypeOf(); + expectTypeOf(config.db.port).toEqualTypeOf(); + expectTypeOf(config.db.connection.url).toEqualTypeOf(); + expectTypeOf(config.db.connection.isLocal).toEqualTypeOf(); + + // Also verify runtime behavior + expect(config.db.host).toBe('localhost'); + expect(config.db.port).toBe(5432); + expect(config.db.connection.url).toBe('postgres://localhost:5432'); + expect(config.db.connection.isLocal).toBe(true); + }); + + it('infers correct types when mixing flat and nested computed values', () => { + const config = createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + }, + api: { + key: envvar('API_KEY', z.string()), + }, + }, + computed: { + isConfigured: (raw) => raw.db.host.length > 0, + db: { + isLocal: (raw) => raw.db.host === 'localhost', + }, + api: { + keyLength: (raw) => raw.api.key.length, + }, + }, + }); + + expectTypeOf(config.db.host).toEqualTypeOf(); + expectTypeOf(config.db.isLocal).toEqualTypeOf(); + expectTypeOf(config.api.key).toEqualTypeOf(); + expectTypeOf(config.api.keyLength).toEqualTypeOf(); + expectTypeOf(config.isConfigured).toEqualTypeOf(); + }); + + it('infers optional schema values correctly', () => { + const config = createConfig(mockEnv, { + schema: { + optional: envvar('MISSING', z.string().optional()), + withDefault: envvar('ALSO_MISSING', z.string().default('default')), + }, + }); + + expectTypeOf(config.optional).toEqualTypeOf(); + expectTypeOf(config.withDefault).toEqualTypeOf(); + }); + + it('infers raw parameter type correctly in computed functions', () => { + createConfig(mockEnv, { + schema: { + db: { + host: envvar('DB_HOST', z.string()), + port: envvar('DB_PORT', z.coerce.number()), + }, + }, + computed: { + test: (raw) => { + // Verify raw parameter has correct type + expectTypeOf(raw.db.host).toEqualTypeOf(); + expectTypeOf(raw.db.port).toEqualTypeOf(); + return true; + }, + }, + }); + }); + }); }); }); diff --git a/src/types.ts b/src/types.ts index 1f64397..c177c2a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type { SimplifyDeep } from 'type-fest'; +import type { MergeDeep, SimplifyDeep } from 'type-fest'; import type { StandardSchemaV1 } from './standard-schema.ts'; export type NodeEnvInfo = { @@ -47,22 +47,7 @@ export type InferComputed = { : never; }; -// Deep merge two types (TComputed values override TRaw where keys overlap) -type DeepMerge = { - [K in keyof TRaw | keyof TComputed]: K extends keyof TComputed - ? K extends keyof TRaw - ? TRaw[K] extends object - ? TComputed[K] extends object - ? DeepMerge - : TComputed[K] - : TComputed[K] - : TComputed[K] - : K extends keyof TRaw - ? TRaw[K] - : never; -}; - // Combined result type (raw config deep merged with computed values) export type InferConfig = SimplifyDeep< - DeepMerge> + MergeDeep> >; diff --git a/vitest.config.ts b/vitest.config.ts index 755d73a..af8c0cf 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,5 +10,8 @@ export default defineConfig({ include: ['src/**/*.ts'], exclude: ['src/index.ts'], }, + typecheck: { + include: ['./src/**/*.test.ts'], + }, }, }); From 724dc92f9e0054330cd00a1c042d226fc5cbfa5c Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Wed, 28 Jan 2026 15:55:43 +0200 Subject: [PATCH 5/5] Add Node 24 to the matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b773a22..865ba79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20, 22] + node-version: [20, 22, 24] steps: - name: Checkout uses: actions/checkout@v6